Coverage for ckanext/udc/tests/graph/test_template.py: 99%

180 statements  

« prev     ^ index     » next       coverage.py v7.7.1, created at 2026-01-19 23:48 +0000

1""" 

2Tests for graph/template.py - JSON-LD template compilation logic. 

3 

4Tests the compile_template and compile_with_temp_value functions that parse 

5the mapping config into JSON-LD data. 

6""" 

7import pytest 

8from unittest.mock import Mock, patch 

9from ckanext.udc.graph.template import ( 

10 compile_template, 

11 compile_with_temp_value, 

12 is_all_attrs_starts_with_at, 

13 filter_out_empty_values 

14) 

15from ckanext.udc.graph.contants import EMPTY_FIELD 

16 

17 

18@pytest.fixture(autouse=True) 

19def mock_udc_plugin(): 

20 """Mock the UDC plugin for all tests.""" 

21 with patch('ckanext.udc.graph.template.get_plugin') as mock_get_plugin: 

22 mock_plugin = Mock() 

23 mock_plugin.text_fields = [] 

24 mock_get_plugin.return_value = mock_plugin 

25 yield mock_plugin 

26 

27 

28class TestIsAllAttrsStartsWithAt: 

29 """Test the is_all_attrs_starts_with_at helper function.""" 

30 

31 def test_all_attrs_start_with_at(self): 

32 """Test when all attributes start with @.""" 

33 data = {"@id": "test", "@type": "Dataset", "@value": "hello"} 

34 assert is_all_attrs_starts_with_at(data) is True 

35 

36 def test_some_attrs_dont_start_with_at(self): 

37 """Test when some attributes don't start with @.""" 

38 data = {"@id": "test", "name": "Dataset", "@value": "hello"} 

39 assert is_all_attrs_starts_with_at(data) is False 

40 

41 def test_empty_dict(self): 

42 """Test with empty dictionary.""" 

43 data = {} 

44 assert is_all_attrs_starts_with_at(data) is True 

45 

46 

47class TestFilterOutEmptyValues: 

48 """Test the filter_out_empty_values function.""" 

49 

50 def test_filter_empty_dicts(self): 

51 """Test filtering out empty dictionaries.""" 

52 data = [{"name": "test"}, {}, {"@id": "uri"}] 

53 result = filter_out_empty_values(data) 

54 assert len(result) == 2 

55 assert {} not in result 

56 

57 def test_filter_only_id_dicts(self): 

58 """Test keeping dictionaries with only @id.""" 

59 data = [{"@id": "uri1"}, {"@id": "uri2", "name": "test"}] 

60 result = filter_out_empty_values(data) 

61 assert len(result) == 2 

62 

63 def test_filter_at_attrs_without_value(self): 

64 """Test filtering @ attributes without @value.""" 

65 data = [ 

66 {"@type": "xsd:string", "@language": "en"}, # No @value 

67 {"@type": "xsd:string", "@value": "hello"}, 

68 ] 

69 result = filter_out_empty_values(data) 

70 assert len(result) == 1 

71 assert result[0]["@value"] == "hello" 

72 

73 

74class TestCompileTemplate: 

75 """Test the compile_template function.""" 

76 

77 def test_simple_string_substitution(self): 

78 """Test simple f-string style substitution.""" 

79 template = ["{name}"] 

80 global_vars = {} 

81 local_vars = {"name": "TestDataset"} 

82 

83 result = compile_template(template, global_vars, local_vars) 

84 assert result == "TestDataset" 

85 

86 def test_empty_string_becomes_empty_field(self): 

87 """Test that empty strings are converted to EMPTY_FIELD.""" 

88 template = ["{name}"] 

89 global_vars = {} 

90 local_vars = {"name": ""} 

91 

92 result = compile_template(template, global_vars, local_vars) 

93 assert result == [] 

94 

95 def test_nested_dict_compilation(self): 

96 """Test compilation of nested dictionary structures.""" 

97 template = [{ 

98 "@id": "http://example.com/{id}", 

99 "@type": "Dataset", 

100 "name": "{title}" 

101 }] 

102 global_vars = {} 

103 local_vars = {"id": "123", "title": "Test Dataset"} 

104 

105 result = compile_template(template, global_vars, local_vars) 

106 assert result["@id"] == "http://example.com/123" 

107 assert result["@type"] == "Dataset" 

108 assert result["name"] == "Test Dataset" 

109 

110 def test_eval_expression(self): 

111 """Test eval() expressions in templates.""" 

112 template = [{ 

113 "@value": "eval(title.upper())" 

114 }] 

115 global_vars = {} 

116 local_vars = {"title": "hello"} 

117 

118 result = compile_template(template, global_vars, local_vars) 

119 assert result == "HELLO" 

120 

121 def test_eval_with_helper_function(self): 

122 """Test eval() with helper functions.""" 

123 def to_uppercase(val): 

124 return val.upper() 

125 

126 template = [{ 

127 "@value": "eval(to_uppercase(name))" 

128 }] 

129 global_vars = {"to_uppercase": to_uppercase} 

130 local_vars = {"name": "dataset"} 

131 

132 result = compile_template(template, global_vars, local_vars) 

133 assert result == "DATASET" 

134 

135 def test_eval_with_text_field_localization(self, mock_udc_plugin): 

136 """Test eval() with localized text fields.""" 

137 # Configure the mock plugin for this test 

138 mock_udc_plugin.text_fields = ["title"] 

139 

140 def map_to_multiple_languages(val): 

141 if isinstance(val, dict): 

142 return [{"@language": lang, "@value": value} 

143 for lang, value in val.items()] 

144 return [{"@language": "en", "@value": val}] 

145 

146 template = [{ 

147 "title": "eval(title)" 

148 }] 

149 global_vars = {"map_to_multiple_languages": map_to_multiple_languages} 

150 local_vars = {"title": {"en": "English Title", "fr": "Titre français"}} 

151 

152 result = compile_template(template, global_vars, local_vars) 

153 assert len(result["title"]) == 2 

154 assert any(item.get("@language") == "en" and item.get("@value") == "English Title" 

155 for item in result["title"]) 

156 assert any(item.get("@language") == "fr" and item.get("@value") == "Titre français" 

157 for item in result["title"]) 

158 

159 def test_nested_list_compilation(self): 

160 """Test compilation of nested lists.""" 

161 template = [{ 

162 "@id": "http://example.com/dataset", 

163 "keywords": [ 

164 {"@value": "{keyword1}"}, 

165 {"@value": "{keyword2}"} 

166 ] 

167 }] 

168 global_vars = {} 

169 local_vars = {"keyword1": "housing", "keyword2": "transport"} 

170 

171 result = compile_template(template, global_vars, local_vars) 

172 assert len(result["keywords"]) == 2 

173 assert result["keywords"][0]["@value"] == "housing" 

174 assert result["keywords"][1]["@value"] == "transport" 

175 

176 def test_remove_empty_nested_attrs(self): 

177 """Test that empty nested attributes are removed.""" 

178 template = [{ 

179 "@id": "http://example.com/dataset", 

180 "title": "{title}", 

181 "description": "{description}" # Will be empty 

182 }] 

183 global_vars = {} 

184 local_vars = {"title": "Test", "description": ""} 

185 

186 result = compile_template(template, global_vars, local_vars) 

187 assert "title" in result 

188 assert "description" not in result 

189 

190 def test_undefined_variable_handling(self): 

191 """Test that undefined variables are handled gracefully.""" 

192 template = [{ 

193 "@id": "http://example.com/dataset", 

194 "name": "{undefined_var}" 

195 }] 

196 global_vars = {} 

197 local_vars = {} 

198 

199 result = compile_template(template, global_vars, local_vars) 

200 assert "name" not in result 

201 

202 def test_complex_nested_structure(self): 

203 """Test complex nested structure compilation.""" 

204 template = [{ 

205 "@id": "http://example.com/catalogue/{id}", 

206 "@type": "Catalogue", 

207 "publisher": [{ 

208 "@id": "http://example.com/org/{org_id}", 

209 "@type": "Organization", 

210 "name": "{org_name}", 

211 "email": "{org_email}" 

212 }] 

213 }] 

214 global_vars = {} 

215 local_vars = { 

216 "id": "cat123", 

217 "org_id": "org456", 

218 "org_name": "Test Org", 

219 "org_email": "test@example.com" 

220 } 

221 

222 result = compile_template(template, global_vars, local_vars) 

223 assert result["@id"] == "http://example.com/catalogue/cat123" 

224 assert len(result["publisher"]) == 1 

225 assert result["publisher"][0]["@id"] == "http://example.com/org/org456" 

226 assert result["publisher"][0]["name"] == "Test Org" 

227 

228 

229class TestCompileWithTempValue: 

230 """Test the compile_with_temp_value function.""" 

231 

232 def test_simple_substitution(self): 

233 """Test simple substitution with available values.""" 

234 mappings = [{"@id": "http://example.com/{id}", "name": "{name}"}] 

235 global_vars = {} 

236 local_vars = {"id": "123", "name": "Test"} 

237 

238 result = compile_with_temp_value(mappings, global_vars, local_vars) 

239 assert result["@id"] == "http://example.com/123" 

240 assert result["name"] == "Test" 

241 

242 def test_temp_value_for_undefined_vars(self): 

243 """Test that undefined variables are replaced with TEMP_VALUE.""" 

244 mappings = [{"@id": "http://example.com/{id}", "name": "{undefined}"}] 

245 global_vars = {} 

246 local_vars = {"id": "123"} 

247 

248 result = compile_with_temp_value(mappings, global_vars, local_vars) 

249 assert result["@id"] == "http://example.com/123" 

250 assert result["name"] == "TEMP_VALUE" 

251 

252 def test_nested_temp_values(self): 

253 """Test temp values in nested structures.""" 

254 mappings = [{ 

255 "@id": "http://example.com/cat", 

256 "creator": [{ 

257 "@id": "http://example.com/person/{person_id}", 

258 "name": "{person_name}" 

259 }] 

260 }] 

261 global_vars = {} 

262 local_vars = {} 

263 

264 result = compile_with_temp_value(mappings, global_vars, local_vars) 

265 assert "TEMP_VALUE" in result["creator"]["@id"] 

266 assert result["creator"]["name"] == "TEMP_VALUE" 

267 

268 def test_preserves_structure(self): 

269 """Test that the structure is preserved even with missing values.""" 

270 mappings = [{ 

271 "@id": "uri", 

272 "title": "{title}", 

273 "description": "{desc}", 

274 "publisher": [{ 

275 "name": "{pub_name}" 

276 }] 

277 }] 

278 global_vars = {} 

279 local_vars = {} 

280 

281 result = compile_with_temp_value(mappings, global_vars, local_vars) 

282 assert "@id" in result 

283 assert "title" in result 

284 assert "description" in result 

285 assert "publisher" in result 

286 assert len(result["publisher"]) == 1 

287 

288 

289class TestRealWorldScenarios: 

290 """Test with real-world-like data and mapping scenarios.""" 

291 

292 def test_catalogue_entry_mapping(self): 

293 """Test mapping a complete catalogue entry.""" 

294 def map_to_multiple_languages(val): 

295 if isinstance(val, dict): 

296 return [{"@language": lang, "@value": value} 

297 for lang, value in val.items()] 

298 return [{"@language": "en", "@value": val}] 

299 

300 template = [{ 

301 "@id": "http://data.urbandatacentre.ca/catalogue/{id}", 

302 "@type": "http://data.urbandatacentre.ca/catalogue", 

303 "dct:title": "eval(map_to_multiple_languages(title))", 

304 "dct:description": "eval(map_to_multiple_languages(description))", 

305 "dct:issued": { 

306 "@type": "xsd:date", 

307 "@value": "{published_date}" 

308 } 

309 }] 

310 

311 global_vars = {"map_to_multiple_languages": map_to_multiple_languages} 

312 local_vars = { 

313 "id": "dataset-001", 

314 "title": {"en": "Housing Data", "fr": "Données sur le logement"}, 

315 "description": {"en": "Housing statistics", "fr": "Statistiques de logement"}, 

316 "published_date": "2025-01-01" 

317 } 

318 

319 result = compile_template(template, global_vars, local_vars) 

320 

321 assert result["@id"] == "http://data.urbandatacentre.ca/catalogue/dataset-001" 

322 assert len(result["dct:title"]) == 2 

323 assert len(result["dct:description"]) == 2 

324 assert result["dct:issued"][0]["@value"] == "2025-01-01" 

325 

326 def test_with_optional_fields(self): 

327 """Test mapping with optional fields that may be empty.""" 

328 template = [{ 

329 "@id": "http://example.com/{id}", 

330 "required_field": "{title}", 

331 "optional_field": "{optional}", 

332 "another_optional": "{also_optional}" 

333 }] 

334 

335 global_vars = {} 

336 local_vars = { 

337 "id": "123", 

338 "title": "Required Title", 

339 "optional": "", # Empty optional 

340 # also_optional is not provided 

341 } 

342 

343 result = compile_template(template, global_vars, local_vars) 

344 

345 assert result["@id"] == "http://example.com/123" 

346 assert result["required_field"] == "Required Title" 

347 assert "optional_field" not in result 

348 assert "another_optional" not in result 

349 

350 def test_uri_list_generation(self): 

351 """Test generating a list of URIs from comma-separated values.""" 

352 def split_to_uris(val, separator=","): 

353 return [{"@id": uri.strip()} for uri in val.split(separator)] 

354 

355 template = [{ 

356 "@id": "http://example.com/dataset", 

357 "formats": "eval(split_to_uris(file_format))" 

358 }] 

359 

360 global_vars = {"split_to_uris": split_to_uris} 

361 local_vars = {"file_format": "csv,json,xml"} 

362 

363 result = compile_template(template, global_vars, local_vars) 

364 

365 assert len(result["formats"]) == 3 

366 assert {"@id": "csv"} in result["formats"] 

367 assert {"@id": "json"} in result["formats"] 

368 assert {"@id": "xml"} in result["formats"]