Coverage for tests/test_navdict.py: 95%

124 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-06 14:03 +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 

23YAML_STRING_SIMPLE = """ 

24Setup: 

25 site_id: KUL 

26  

27 gse: 

28 hexapod: 

29 id: PUNA_01 

30 

31""" 

32 

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

41 

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

54 

55YAML_STRING_WITH_UNKNOWN_CLASS = """ 

56root: 

57 part_one: 

58 cls: class//navdict.navdict 

59 part_two: 

60 cls: class//unknown.navdict 

61""" 

62 

63YAML_STRING_INVALID_INDENTATION = """ 

64name: test 

65 age: 30 

66description: invalid indentation 

67""" 

68 

69YAML_STRING_MISSING_COLON = """ 

70name test 

71age: 30 

72""" 

73 

74YAML_STRING_EMPTY = """""" 

75 

76 

77def test_construction(): 

78 

79 setup = navdict() 

80 

81 assert setup == {} 

82 assert setup.label is None 

83 

84 setup = navdict(label="Setup") 

85 assert setup.label == "Setup" 

86 

87 

88def test_from_yaml_string(): 

89 

90 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

91 

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" 

96 

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) 

99 

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) 

102 

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) 

105 

106 

107def test_from_yaml_file(): 

108 

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" 

115 

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) 

120 

121 assert "root" in data 

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

123 

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 

127 

128 

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

134 

135 setup = navdict.from_yaml_string(YAML_STRING_SIMPLE) 

136 setup.to_yaml_file("simple.yaml") 

137 

138 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

139 setup.to_yaml_file("with_class.yaml") 

140 

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

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

143 

144 

145def test_class_directive(): 

146 

147 setup = navdict.from_yaml_string(YAML_STRING_WITH_CLASS) 

148 

149 obj = setup.root.defaults.dev 

150 assert isinstance(obj, TakeTwoOptionalArguments) 

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

152 

153 obj = setup.root.with_args.dev 

154 assert isinstance(obj, TakeTwoOptionalArguments) 

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

156 

157 

158def test_from_dict(): 

159 

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

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

162 

163 assert setup._label == "Setup" 

164 

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 

168 

169 with pytest.raises(AttributeError): 

170 _ = setup.ID 

171 

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" 

176 

177 with pytest.raises(AttributeError): 

178 _ = setup.answer.book 

179 

180 

181def get_enum_metaclass(): 

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

183 if hasattr(enum, 'EnumMeta'): 183 ↛ 185line 183 didn't jump to line 185 because the condition on line 183 was always true

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) 

190 

191 

192def test_int_enum(): 

193 

194 setup = navdict.from_yaml_string(YAML_STRING_WITH_INT_ENUM) 

195 

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 

200 

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' 

205 

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' 

210 

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 

215 

216 

217YAML_STRING_LOADS_YAML_FILE = """ 

218root: 

219 simple: yaml//enum.yaml 

220""" 

221 

222 

223def test_recursive_load(): 

224 

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 

231 

232 

233YAML_STRING_LOADS_CSV_FILE = """ 

234root: 

235 sample: csv//sample.csv 

236 kwargs: 

237 header_rows: 2 

238""" 

239 

240 

241def test_load_csv(): 

242 

243 with ( 

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

245 create_test_csv_file("sample.csv") 

246 ): 

247 

248 data = navdict.from_yaml_file(fn) 

249 

250 header, csv_data = data.root.sample 

251 

252 assert len(header) == 2 

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

254 assert isinstance(header, list) 

255 assert isinstance(header[0], list) 

256 

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

258 assert isinstance(csv_data, list) 

259 assert isinstance(csv_data[0], list) 

260 

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

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

263 

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

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