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

1import enum 

2from pathlib import Path 

3 

4import pytest 

5 

6from navdict import navdict 

7from tests.helpers import create_test_csv_file 

8from tests.helpers import create_text_file 

9 

10 

11class TakeTwoOptionalArguments: 

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

13 

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

15 super().__init__() 

16 self._a = a 

17 self._b = b 

18 

19 def __str__(self): 

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

21 

22 

23class TakeOneKeywordArgument: 

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

25 self._sim = sim 

26 

27 def __str__(self): 

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

29 

30 

31YAML_STRING_SIMPLE = """ 

32Setup: 

33 site_id: KUL 

34  

35 gse: 

36 hexapod: 

37 id: PUNA_01 

38 

39""" 

40 

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""" 

53 

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""" 

66 

67YAML_STRING_WITH_UNKNOWN_CLASS = """ 

68root: 

69 part_one: 

70 cls: class//navdict.navdict 

71 part_two: 

72 cls: class//unknown.navdict 

73""" 

74 

75YAML_STRING_INVALID_INDENTATION = """ 

76name: test 

77 age: 30 

78description: invalid indentation 

79""" 

80 

81YAML_STRING_MISSING_COLON = """ 

82name test 

83age: 30 

84""" 

85 

86YAML_STRING_EMPTY = """""" 

87 

88 

89def test_construction(): 

90 

91 setup = navdict() 

92 

93 assert setup == {} 

94 assert setup.label is None 

95 

96 setup = navdict(label="Setup") 

97 assert setup.label == "Setup" 

98 

99 

100def test_from_yaml_string(): 

101 

102 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

103 

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" 

108 

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) 

111 

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) 

114 

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) 

117 

118 

119def test_from_yaml_file(): 

120 

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" 

127 

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) 

132 

133 assert "root" in data 

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

135 

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 

139 

140 

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 """ 

146 

147 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

148 setup.to_yaml_file("simple.yaml") 

149 

150 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

151 setup.to_yaml_file("with_class.yaml") 

152 

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

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

155 

156 

157def test_class_directive(): 

158 

159 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

160 

161 obj = setup.root.defaults.dev 

162 assert isinstance(obj, TakeTwoOptionalArguments) 

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

164 

165 obj = setup.root.with_args.dev 

166 assert isinstance(obj, TakeTwoOptionalArguments) 

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

168 

169 obj = setup.root.with_kwarg.dev 

170 assert isinstance(obj, TakeOneKeywordArgument) 

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

172 

173 

174def test_from_dict(): 

175 

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

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

178 

179 assert setup._label == "Setup" 

180 

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 

184 

185 with pytest.raises(AttributeError): 

186 _ = setup.ID 

187 

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" 

192 

193 with pytest.raises(AttributeError): 

194 _ = setup.answer.book 

195 

196 

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) 

206 

207 

208def test_int_enum(): 

209 

210 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM) 

211 

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 

216 

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' 

221 

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' 

226 

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 

231 

232 

233YAML_STRING_LOADS_YAML_FILE = """ 

234root: 

235 simple: yaml//enum.yaml 

236""" 

237 

238 

239def test_recursive_load(): 

240 

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 

247 

248 

249YAML_STRING_LOADS_CSV_FILE = """ 

250root: 

251 sample: csv//sample.csv 

252 sample_kwargs: 

253 header_rows: 2 

254""" 

255 

256 

257def test_load_csv(): 

258 

259 with ( 

260 create_text_file("load_csv.yaml", YAML_STRING_LOADS_CSV_FILE) as fn, 

261 create_test_csv_file("sample.csv") 

262 ): 

263 

264 data = navdict.from_yaml_file(fn) 

265 

266 header, csv_data = data.root.sample 

267 

268 assert len(header) == 2 

269 assert len(header[0]) == 9 

270 assert isinstance(header, list) 

271 assert isinstance(header[0], list) 

272 

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

274 assert isinstance(csv_data, list) 

275 assert isinstance(csv_data[0], list) 

276 

277 assert header[0][3] == "department" 

278 assert header[1][0] == "# a comment line" 

279 

280 assert csv_data[0][0] == '1001' 

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