Skip to content

Commit

Permalink
Fix simple literals returned as NULL using SERVICE (issue #1278) (#1894)
Browse files Browse the repository at this point in the history
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 <mark.van.der.pas@semaku.com>
Co-authored-by: Iwan Aucamp <aucampia@gmail.com>
  • Loading branch information
3 people committed May 15, 2022
1 parent 246c887 commit e9908b4
Show file tree
Hide file tree
Showing 2 changed files with 205 additions and 15 deletions.
24 changes: 16 additions & 8 deletions rdflib/plugins/sparql/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)


Expand Down
196 changes: 189 additions & 7 deletions test/test_sparql/test_service.py
Original file line number Diff line number Diff line change
@@ -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():
Expand Down Expand Up @@ -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 <https://DBpedia.org/sparql> {
VALUES (?s ?p ?o) {(<http://example.org/a> <http://example.org/b> "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 <https://dbpedia.org/sparql> {
VALUES (?s ?p ?o) {
(<http://example.org/a> <http://example.org/uri> <http://example.org/URI>)
(<http://example.org/a> <http://example.org/simpleLiteral> "Simple Literal")
(<http://example.org/a> <http://example.org/dataType> "String Literal"^^xsd:string)
(<http://example.org/a> <http://example.org/language> "String Language"@en)
(<http://example.org/a> <http://example.org/language> "String Language"@en)
}
}
FILTER( ?o IN (<http://example.org/URI>, "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: <http://example.org/>
SELECT ?var
WHERE {
SERVICE <REMOTE_URL> {
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__":
Expand All @@ -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()

0 comments on commit e9908b4

Please sign in to comment.