From e9908b4bb9ea79a96f61ddde094a1c0e1bd90d27 Mon Sep 17 00:00:00 2001 From: gitmpje <61799691+gitmpje@users.noreply.github.com> Date: Sun, 15 May 2022 15:47:42 +0200 Subject: [PATCH] Fix simple literals returned as NULL using SERVICE (issue #1278) (#1894) Fixes #1278 simple literals returned as NULL. The resolution uses same logic as here: https://github.com/RDFLib/rdflib/blob/6f2c11cd2c549d6410f9a1c948ab3a8dbf77ca00/rdflib/plugins/sparql/results/jsonresults.py#L89-L107 Co-authored-by: Mark van der Pas Co-authored-by: Iwan Aucamp --- rdflib/plugins/sparql/evaluate.py | 24 ++-- test/test_sparql/test_service.py | 196 ++++++++++++++++++++++++++++-- 2 files changed, 205 insertions(+), 15 deletions(-) diff --git a/rdflib/plugins/sparql/evaluate.py b/rdflib/plugins/sparql/evaluate.py index edd322e56..49bff9432 100644 --- a/rdflib/plugins/sparql/evaluate.py +++ b/rdflib/plugins/sparql/evaluate.py @@ -405,18 +405,26 @@ def _yieldBindingsFromServiceCallResult( res_dict: Dict[Variable, Identifier] = {} for var in variables: if var in r and r[var]: - if r[var]["type"] == "uri": - res_dict[Variable(var)] = URIRef(r[var]["value"]) - elif r[var]["type"] == "bnode": - res_dict[Variable(var)] = BNode(r[var]["value"]) - elif r[var]["type"] == "literal" and "datatype" in r[var]: + var_binding = r[var] + var_type = var_binding["type"] + if var_type == "uri": + res_dict[Variable(var)] = URIRef(var_binding["value"]) + elif var_type == "literal": res_dict[Variable(var)] = Literal( - r[var]["value"], datatype=r[var]["datatype"] + var_binding["value"], + datatype=var_binding.get("datatype"), + lang=var_binding.get("xml:lang"), ) - elif r[var]["type"] == "literal" and "xml:lang" in r[var]: + # This is here because of + # https://www.w3.org/TR/2006/NOTE-rdf-sparql-json-res-20061004/#variable-binding-results + elif var_type == "typed-literal": res_dict[Variable(var)] = Literal( - r[var]["value"], lang=r[var]["xml:lang"] + var_binding["value"], datatype=URIRef(var_binding["datatype"]) ) + elif var_type == "bnode": + res_dict[Variable(var)] = BNode(var_binding["value"]) + else: + raise ValueError(f"invalid type {var_type!r} for variable {var!r}") yield FrozenBindings(ctx, res_dict) diff --git a/test/test_sparql/test_service.py b/test/test_sparql/test_service.py index 9798f3df8..31ba6e8a5 100644 --- a/test/test_sparql/test_service.py +++ b/test/test_sparql/test_service.py @@ -1,8 +1,31 @@ +import json +from contextlib import ExitStack from test.utils import helper +from test.utils.httpservermock import ( + MethodName, + MockHTTPResponse, + ServedBaseHTTPServerMock, +) +from typing import ( + Dict, + FrozenSet, + Generator, + List, + Mapping, + Optional, + Sequence, + Tuple, + Type, + Union, +) + +import pytest from rdflib import Graph, Literal, URIRef, Variable from rdflib.compare import isomorphic +from rdflib.namespace import XSD from rdflib.plugins.sparql import prepareQuery +from rdflib.term import BNode, Identifier def test_service(): @@ -135,13 +158,171 @@ def test_service_with_implicit_select_and_allcaps(): assert len(results) == 3 -# def test_with_fixture(httpserver): -# httpserver.expect_request("/sparql/?query=SELECT * WHERE ?s ?p ?o").respond_with_json({"vars": ["s","p","o"], "bindings":[]}) -# test_server = httpserver.url_for('/sparql') -# g = Graph() -# q = 'SELECT * WHERE {SERVICE <'+test_server+'>{?s ?p ?o} . ?s ?p ?o .}' -# results = g.query(q) -# assert len(results) == 0 +def freeze_bindings( + bindings: Sequence[Mapping[Variable, Identifier]] +) -> FrozenSet[FrozenSet[Tuple[Variable, Identifier]]]: + result = [] + for binding in bindings: + result.append(frozenset(((key, value)) for key, value in binding.items())) + return frozenset(result) + + +def test_simple_not_null(): + """Test service returns simple literals not as NULL. + + Issue: https://github.com/RDFLib/rdflib/issues/1278 + """ + + g = Graph() + q = """SELECT ?s ?p ?o +WHERE { + SERVICE { + VALUES (?s ?p ?o) {( "c")} + } +}""" + results = helper.query_with_retry(g, q) + assert results.bindings[0].get(Variable("o")) == Literal("c") + + +def test_service_node_types(): + """Test if SERVICE properly returns different types of nodes: + - URI; + - Simple Literal; + - Literal with datatype ; + - Literal with language tag . + """ + + g = Graph() + q = """ +SELECT ?o +WHERE { + SERVICE { + VALUES (?s ?p ?o) { + ( ) + ( "Simple Literal") + ( "String Literal"^^xsd:string) + ( "String Language"@en) + ( "String Language"@en) + } + } + FILTER( ?o IN (, "Simple Literal", "String Literal"^^xsd:string, "String Language"@en) ) +}""" + results = helper.query_with_retry(g, q) + + expected = freeze_bindings( + [ + {Variable('o'): URIRef('http://example.org/URI')}, + {Variable('o'): Literal('Simple Literal')}, + { + Variable('o'): Literal( + 'String Literal', + datatype=URIRef('http://www.w3.org/2001/XMLSchema#string'), + ) + }, + {Variable('o'): Literal('String Language', lang='en')}, + ] + ) + assert expected == freeze_bindings(results.bindings) + + +@pytest.fixture(scope="module") +def module_httpmock() -> Generator[ServedBaseHTTPServerMock, None, None]: + with ServedBaseHTTPServerMock() as httpmock: + yield httpmock + + +@pytest.fixture(scope="function") +def httpmock( + module_httpmock: ServedBaseHTTPServerMock, +) -> Generator[ServedBaseHTTPServerMock, None, None]: + module_httpmock.reset() + yield module_httpmock + + +@pytest.mark.parametrize( + ("response_bindings", "expected_result"), + [ + ( + [ + {"type": "uri", "value": "http://example.org/uri"}, + {"type": "literal", "value": "literal without type or lang"}, + {"type": "literal", "value": "literal with lang", "xml:lang": "en"}, + { + "type": "typed-literal", + "value": "typed-literal with datatype", + "datatype": f"{XSD.string}", + }, + { + "type": "literal", + "value": "literal with datatype", + "datatype": f"{XSD.string}", + }, + {"type": "bnode", "value": "ohci6Te6aidooNgo"}, + ], + [ + URIRef('http://example.org/uri'), + Literal('literal without type or lang'), + Literal('literal with lang', lang='en'), + Literal( + 'typed-literal with datatype', + datatype=URIRef('http://www.w3.org/2001/XMLSchema#string'), + ), + Literal('literal with datatype', datatype=XSD.string), + BNode('ohci6Te6aidooNgo'), + ], + ), + ( + [ + {"type": "invalid-type"}, + ], + ValueError, + ), + ], +) +def test_with_mock( + httpmock: ServedBaseHTTPServerMock, + response_bindings: List[Dict[str, str]], + expected_result: Union[List[Identifier], Type[Exception]], +) -> None: + """ + This tests that bindings for a variable named var + """ + graph = Graph() + query = """ + PREFIX ex: + SELECT ?var + WHERE { + SERVICE { + ex:s ex:p ?var + } + } + """ + query = query.replace("REMOTE_URL", httpmock.url) + response = { + "head": {"vars": ["var"]}, + "results": {"bindings": [{"var": item} for item in response_bindings]}, + } + httpmock.responses[MethodName.GET].append( + MockHTTPResponse( + 200, + "OK", + json.dumps(response).encode("utf-8"), + {"Content-Type": ["application/sparql-results+json"]}, + ) + ) + catcher: Optional[pytest.ExceptionInfo[Exception]] = None + + with ExitStack() as xstack: + if isinstance(expected_result, type) and issubclass(expected_result, Exception): + catcher = xstack.enter_context(pytest.raises(expected_result)) + else: + expected_bindings = [{Variable("var"): item} for item in expected_result] + bindings = graph.query(query).bindings + if catcher is not None: + assert catcher is not None + assert catcher.value is not None + else: + assert expected_bindings == bindings if __name__ == "__main__": @@ -151,3 +332,4 @@ def test_service_with_implicit_select_and_allcaps(): test_service_with_implicit_select() test_service_with_implicit_select_and_prefix() test_service_with_implicit_select_and_base() + test_service_node_types()