Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix simple literals returned as NULL using SERVICE (issue #1278) #1894

Merged
merged 14 commits into from
May 15, 2022
33 changes: 22 additions & 11 deletions rdflib/plugins/sparql/evaluate.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
import itertools
import json as j
import re
from typing import Any, Deque, Dict, List, Union
from typing import Any, Deque, Dict, Generator, List, Sequence, Union
from urllib.parse import urlencode
from urllib.request import Request, urlopen

Expand Down Expand Up @@ -337,7 +337,8 @@ def evalServiceQuery(ctx: QueryContext, part):
res = json["results"]["bindings"]
if len(res) > 0:
for r in res:
for bound in _yieldBindingsFromServiceCallResult(ctx, r, variables):
# type error: Argument 2 to "_yieldBindingsFromServiceCallResult" has incompatible type "str"; expected "Dict[str, Dict[str, str]]"
for bound in _yieldBindingsFromServiceCallResult(ctx, r, variables): # type: ignore[arg-type]
yield bound
else:
raise Exception(
Expand Down Expand Up @@ -377,22 +378,32 @@ def _buildQueryStringForServiceCall(ctx: QueryContext, match):
return service_query


def _yieldBindingsFromServiceCallResult(ctx: QueryContext, r, variables):
def _yieldBindingsFromServiceCallResult(
ctx: QueryContext, r: Dict[str, Dict[str, str]], variables: List[str]
) -> Generator[FrozenBindings, None, None]:
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()