Coverage for test_navdict.py: 29%
115 statements
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-06 12:04 +0200
« prev ^ index » next coverage.py v7.8.2, created at 2025-06-06 12:04 +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}"
23YAML_STRING_SIMPLE = """
24Setup:
25 site_id: KUL
27 gse:
28 hexapod:
29 id: PUNA_01
31"""
33YAML_STRING_WITH_CLASS = """
34root:
35 defaults:
36 dev: class//test_navdict.TakeTwoOptionalArguments
37 with_args:
38 dev: class//test_navdict.TakeTwoOptionalArguments
39 dev_args: [42, 73]
40"""
42YAML_STRING_WITH_INT_ENUM = """
43F_FEE:
44 ccd_sides:
45 enum: int_enum//FEE_SIDES
46 content:
47 E:
48 alias: ['E_SIDE', 'RIGHT_SIDE']
49 value: 1
50 F:
51 alias: ['F_SIDE', 'LEFT_SIDE']
52 value: 0
53"""
55YAML_STRING_WITH_UNKNOWN_CLASS = """
56root:
57 part_one:
58 cls: class//navdict.navdict
59 part_two:
60 cls: class//unknown.navdict
61"""
63YAML_STRING_INVALID_INDENTATION = """
64name: test
65 age: 30
66description: invalid indentation
67"""
69YAML_STRING_MISSING_COLON = """
70name test
71age: 30
72"""
74YAML_STRING_EMPTY = """"""
77def test_construction():
79 setup = navdict()
81 assert setup == {}
82 assert setup.label is None
84 setup = navdict(label="Setup")
85 assert setup.label == "Setup"
88def test_from_yaml_string():
90 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
92 assert "Setup" in setup
93 assert "site_id" in setup.Setup
94 assert "gse" in setup.Setup
95 assert setup.Setup.gse.hexapod.id == "PUNA_01"
97 with pytest.raises(ValueError, match="Invalid YAML string: mapping values are not allowed in this context"):
98 setup = navdict.from_yaml_string(YAML_STRING_INVALID_INDENTATION)
100 with pytest.raises(ValueError, match="Invalid YAML string: mapping values are not allowed in this context"):
101 setup = navdict.from_yaml_string(YAML_STRING_MISSING_COLON)
103 with pytest.raises(ValueError, match="Invalid argument to function: No input string or None given"):
104 setup = navdict.from_yaml_string(YAML_STRING_EMPTY)
107def test_from_yaml_file():
109 with create_text_file("simple.yaml", YAML_STRING_SIMPLE) as fn:
110 setup = navdict.from_yaml_file(fn)
111 assert "Setup" in setup
112 assert "site_id" in setup.Setup
113 assert "gse" in setup.Setup
114 assert setup.Setup.gse.hexapod.id == "PUNA_01"
116 with create_text_file("with_unknown_class.yaml", YAML_STRING_WITH_UNKNOWN_CLASS) as fn:
117 # The following line shall not generate an exception, meaning the `class//`
118 # shall not be evaluated on load!
119 data = navdict.from_yaml_file(fn)
121 assert "root" in data
122 assert isinstance(data.root.part_one.cls, navdict)
124 # Only when accessed, it will generate an exception.
125 with pytest.raises(ModuleNotFoundError, match="No module named 'unknown'"):
126 _ = data.root.part_two.cls
129def test_to_yaml_file():
130 """
131 This test loads the standard Setup and saves it without change to a new file.
132 Loading back the saved Setup should show no differences.
133 """
135 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE)
136 setup.to_yaml_file("simple.yaml")
138 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
139 setup.to_yaml_file("with_class.yaml")
141 Path("simple.yaml").unlink()
142 Path("with_class.yaml").unlink()
145def test_class_directive():
147 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS)
149 obj = setup.root.defaults.dev
150 assert isinstance(obj, TakeTwoOptionalArguments)
151 assert str(obj) == "a=23, b=24"
153 obj = setup.root.with_args.dev
154 assert isinstance(obj, TakeTwoOptionalArguments)
155 assert str(obj) == "a=42, b=73"
158def test_from_dict():
160 setup = navdict.from_dict({"ID": "my-setup-001", "version": "0.1.0"}, label="Setup")
161 assert setup["ID"] == setup.ID == "my-setup-001"
163 assert setup._label == "Setup"
165 # If not all keys are of type 'str', the navdict will not be navigable.
166 setup = navdict.from_dict({"ID": 1234, 42: "forty two"}, label="Setup")
167 assert setup["ID"] == 1234
169 with pytest.raises(AttributeError):
170 _ = setup.ID
172 # Only the (sub-)dictionary that contains non-str keys will not be navigable.
173 setup = navdict.from_dict({"ID": 1234, "answer": {"book": "H2G2", 42: "forty two"}}, label="Setup")
174 assert setup["ID"] == setup.ID == 1234
175 assert setup.answer["book"] == "H2G2"
177 with pytest.raises(AttributeError):
178 _ = setup.answer.book
181def get_enum_metaclass():
182 """Get the enum metaclass in a version-compatible way."""
183 if hasattr(enum, 'EnumMeta'):
184 return enum.EnumMeta
185 elif hasattr(enum, 'EnumType'): # Python 3.11+
186 return enum.EnumType
187 else:
188 # Fallback: get it from a known enum
189 return type(enum.IntEnum)
192def test_int_enum():
194 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM)
196 assert "enum" in setup.F_FEE.ccd_sides
197 assert "content" in setup.F_FEE.ccd_sides
198 assert "E" in setup.F_FEE.ccd_sides.content
199 assert "F" in setup.F_FEE.ccd_sides.content
201 assert setup.F_FEE.ccd_sides.enum.E.value == 1
202 assert setup.F_FEE.ccd_sides.enum.E_SIDE.value == 1
203 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.value == 1
204 assert setup.F_FEE.ccd_sides.enum.RIGHT_SIDE.name == 'E'
206 assert setup.F_FEE.ccd_sides.enum.F.value == 0
207 assert setup.F_FEE.ccd_sides.enum.F_SIDE.value == 0
208 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.value == 0
209 assert setup.F_FEE.ccd_sides.enum.LEFT_SIDE.name == 'F'
211 assert issubclass(setup.F_FEE.ccd_sides.enum, enum.IntEnum)
212 assert isinstance(setup.F_FEE.ccd_sides.enum, get_enum_metaclass())
213 assert isinstance(setup.F_FEE.ccd_sides.enum, type)
214 assert isinstance(setup.F_FEE.ccd_sides.enum.E, enum.IntEnum) # noqa
217YAML_STRING_LOADS_YAML_FILE = """
218root:
219 simple: yaml//enum.yaml
220"""
223def test_recursive_load():
225 with (
226 create_text_file("load_yaml.yaml", YAML_STRING_LOADS_YAML_FILE) as fn,
227 create_text_file("enum.yaml", YAML_STRING_WITH_INT_ENUM)
228 ):
229 data = navdict.from_yaml_file(fn)
230 assert data.root.simple.F_FEE.ccd_sides.enum.E.value == 1
233YAML_STRING_LOADS_CSV_FILE = """
234root:
235 sample: csv//sample.csv
236"""
239def test_load_csv():
241 with (
242 create_text_file("load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn,
243 create_test_csv_file("sample.csv")
244 ):
246 data = navdict.from_yaml_file("load_csv.yaml")
248 csv_data = data.root.sample
250 for line in csv_data:
251 print(line)