Skip to content

Commit

Permalink
Improved the handling of different return types from the query() meth…
Browse files Browse the repository at this point in the history
…od (#219)

# Description
Improved and documented the handling of different return types from the
query() method.

Note that not all lines that codecov complains about are possible to
test simultaneously, because the SPARQL interface in
tripper.backends.rdflib supports multiple versions of rdflib.

## Type of change
- [x] Bug fix and code cleanup
- [ ] New feature
- [ ] Documentation update
- [ ] Testing


## Checklist for the reviewer
This checklist should be used as a help for the reviewer.

- [ ] Is the change limited to one issue?
- [ ] Does this PR close the issue?
- [ ] Is the code easy to read and understand?
- [ ] Do all new feature have an accompanying new test?
- [ ] Has the documentation been updated as necessary?
- [ ] Is the code properly tested?

---------

Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Jun 22, 2024
1 parent cbcccab commit f7488f5
Show file tree
Hide file tree
Showing 7 changed files with 266 additions and 40 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ line_length = 79 # PEP8
line-length = 79

[tool.mypy]
python_version = "3.7"
python_version = "3.11"
ignore_missing_imports = true
scripts_are_modules = true
warn_unused_configs = true
Expand Down
9 changes: 8 additions & 1 deletion tests/test_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

def test_untyped() -> None:
"""Test creating a untyped literal."""
import pytest

from tripper.literal import RDF, XSD, Literal

literal = Literal("Hello world!")
Expand All @@ -19,9 +21,14 @@ def test_untyped() -> None:
assert literal == Literal("Hello world!", datatype=XSD.string)
assert literal == Literal("Hello world!", datatype=XSD.token)
assert literal == Literal("Hello world!", datatype=RDF.JSON)
assert literal != Literal("Hello world!", datatype=XSD.ENTITY)
assert literal == Literal("Hello world!", lang="en")

# Check two things here:
# 1) that a plain literal compares false to a non-string literal
# 2) that we get a warning about unknown XSD.ENTITY datatype
with pytest.warns(UserWarning):
assert literal != Literal("Hello world!", datatype=XSD.ENTITY)


def test_string() -> None:
"""Test creating a string literal."""
Expand Down
179 changes: 176 additions & 3 deletions tests/test_sparql.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@


# if True:
def test_sparql():
"""Test SPARQL query."""
def test_sparql_select():
"""Test SPARQL SELECT query."""
pytest.importorskip("rdflib")
from tripper import Triplestore

Expand Down Expand Up @@ -76,7 +76,7 @@ def test_sparql_construct():
ts.parse(data=data)
VCARD = ts.bind("vcard", "http://www.w3.org/2001/vcard-rdf/3.0#")

r = ts.query(query)
r = list(ts.query(query))

assert len(r) == 6
assert len([s for s, p, o in r if p == VCARD.givenName]) == 2
Expand All @@ -86,3 +86,176 @@ def test_sparql_construct():
assert (
len([s for s, p, o in r if p == VCARD.givenName and o == "Cyril"]) == 0
)


# if True:
def test_sparql_select2():
"""Test SPARQL SELECT query."""
# From https://www.w3.org/TR/rdf-sparql-query/#select
pytest.importorskip("rdflib")
from textwrap import dedent

from tripper import Triplestore

data = dedent(
"""
@prefix : <http://persons.com#> .
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
:allice foaf:name "Alice" .
:allice foaf:knows :bob .
:allice foaf:knows :clare .
:bob foaf:name "Bob" .
:clare foaf:name "Clare" .
:clare foaf:nick "CT" .
"""
)
query = dedent(
"""
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
SELECT ?nameX ?nameY ?nickY
WHERE
{ ?x foaf:knows ?y ;
foaf:name ?nameX .
?y foaf:name ?nameY .
OPTIONAL { ?y foaf:nick ?nickY }
}
"""
)
ts = Triplestore("rdflib")
ts.parse(data=data)
r = ts.query(query)

assert set(r) == {("Alice", "Bob", "None"), ("Alice", "Clare", "CT")}


# if True:
def test_sparql_construct2():
"""Test SPARQL CONSTRUCT query."""
# From https://www.w3.org/TR/rdf-sparql-query/#construct
pytest.importorskip("rdflib")
from textwrap import dedent

from tripper import Literal, Triplestore

# Load pre-inferred EMMO
ts = Triplestore("rdflib")

data = dedent(
"""
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
_:a foaf:name "Alice" .
_:a foaf:mbox <mailto:alice@example.org> .
"""
)
query = dedent(
"""
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
PREFIX vcard: <http://www.w3.org/2001/vcard-rdf/3.0#>
CONSTRUCT { <http://example.org/person#Alice> vcard:FN ?name }
WHERE { ?x foaf:name ?name }
"""
)
ts = Triplestore("rdflib")
ts.parse(data=data)
r = ts.query(query)

assert set(r) == {
(
"http://example.org/person#Alice",
"http://www.w3.org/2001/vcard-rdf/3.0#FN",
Literal("Alice"),
)
}


# if True:
def test_sparql_ask():
"""Test SPARQL ASK query."""
# From https://www.w3.org/TR/rdf-sparql-query/#ask
pytest.importorskip("rdflib")
from textwrap import dedent

from tripper import Triplestore

# Load pre-inferred EMMO
ts = Triplestore("rdflib")

data = dedent(
"""
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
_:a foaf:name "Alice" .
_:a foaf:homepage <http://work.example.org/alice/> .
_:b foaf:name "Bob" .
_:b foaf:mbox <mailto:bob@work.example> .
"""
)
query = dedent(
"""
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
ASK { ?x foaf:name "Alice" }
"""
)
ts = Triplestore("rdflib")
ts.parse(data=data)
r = ts.query(query)
assert r is True


# if True:
def test_sparql_describe():
"""Test SPARQL DESCRIBE query."""
# From https://www.w3.org/TR/rdf-sparql-query/#describe
pytest.importorskip("rdflib")
from textwrap import dedent

from tripper import Literal, Triplestore

# Load pre-inferred EMMO
ts = Triplestore("rdflib")

data = dedent(
"""
@prefix foaf: <http://xmlns.com/foaf/0.1/> .
@prefix vcard: <http://www.w3.org/2001/vcard-rdf/3.0> .
@prefix exOrg: <http://org.example.com/employees#> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
exOrg:Allice
exOrg:employeeId "1234" ;
foaf:mbox_sha1sum "ABCD1234" .
foaf:mbox_sha1sum rdf:type owl:InverseFunctionalProperty .
"""
)
query = dedent(
"""
PREFIX foaf: <http://xmlns.com/foaf/0.1/>
DESCRIBE ?x
WHERE { ?x foaf:mbox_sha1sum "ABCD1234" }
"""
)
ts = Triplestore("rdflib")
ts.parse(data=data)
r = ts.query(query)

assert set(r) == set(
[
(
"http://org.example.com/employees#Allice",
"http://xmlns.com/foaf/0.1/mbox_sha1sum",
Literal("ABCD1234"),
),
(
"http://org.example.com/employees#Allice",
"http://org.example.com/employees#employeeId",
Literal("1234"),
),
]
)
3 changes: 1 addition & 2 deletions tests/test_triplestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,12 +415,11 @@ def test_find_literal_triples() -> None:
ts.triples(predicate=FAM.hasName, object=Literal("Per"))
) == set(
[
(FAM.Per, FAM.hasName, Literal("Per", datatype=XSD.string)),
(FAM.Per, FAM.hasName, Literal("Per")),
]
)


# if True:
def test_bind_errors():
"""Test for errors in Triplestore.bind()."""
pytest.importorskip("rdflib")
Expand Down
90 changes: 63 additions & 27 deletions tripper/backends/rdflib.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

# pylint: disable=line-too-long
import warnings
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Generator

try:
import rdflib # pylint: disable=unused-import
Expand All @@ -26,7 +26,7 @@

if TYPE_CHECKING: # pragma: no cover
from collections.abc import Sequence
from typing import Generator, List, Optional, Tuple, Union
from typing import List, Optional, Tuple, Union

from tripper.triplestore import Triple

Expand Down Expand Up @@ -71,7 +71,7 @@ def __init__(
database: "Optional[str]" = None,
triplestore_url: "Optional[str]" = None,
format: "Optional[str]" = None, # pylint: disable=redefined-builtin
graph: "Graph" = None,
graph: "Optional[Graph]" = None,
) -> None:
# Note that although `base_iri` is unused in this backend, it may
# still be used by calling Triplestore object.
Expand All @@ -88,26 +88,9 @@ def __init__(

def triples(self, triple: "Triple") -> "Generator[Triple, None, None]":
"""Returns a generator over matching triples."""
for s, p, o in self.graph.triples( # pylint: disable=not-an-iterable
astriple(triple)
):
yield (
(
f"_:{s}"
if isinstance(s, BNode) and not s.startswith("_:")
else str(s)
),
str(p),
(
parse_literal(o.n3())
if isinstance(o, rdflibLiteral)
else (
f"_:{o}"
if isinstance(o, BNode) and not o.startswith("_:")
else str(o)
)
),
)
return _convert_triples_to_tripper(
self.graph.triples(astriple(triple))
)

def add_triples(self, triples: "Sequence[Triple]"):
"""Add a sequence of triples."""
Expand Down Expand Up @@ -182,19 +165,49 @@ def serialize(
return result if isinstance(result, str) else result.decode()
return None

def query(self, query_object, **kwargs) -> "List[Tuple[str, ...]]":
def query(
self, query_object, **kwargs
) -> "Union[List[Tuple[str, ...]], bool, Generator[Triple, None, None]]":
"""SPARQL query.
Parameters:
query_object: String with the SPARQL query.
kwargs: Keyword arguments passed to rdflib.Graph.query().
Returns:
List of tuples of IRIs for each matching row.
The return type depends on type of query:
- SELECT: list of tuples of IRIs for each matching row
- ASK: bool
- CONSTRUCT, DESCRIBE: generator over triples
For more info, see
https://rdflib.readthedocs.io/en/stable/apidocs/rdflib.html#rdflib.query.Result
"""
rows = self.graph.query(query_object=query_object, **kwargs)
return [tuple(str(v) for v in row) for row in rows]
result = self.graph.query(query_object=query_object, **kwargs)

# The type of the result object depends not only on the type of query,
# but also on the version of rdflib... We try to be general here.
if hasattr(result, "type"):
resulttype = result.type
elif result.__class__.__name__ == "ResultRow":
resulttype = "SELECT"
elif isinstance(result, bool):
resulttype = "ASK"
elif isinstance(result, Generator):
resulttype = "CONSTRUCT" # also DESCRIBE
else:
warnings.warn(
"Unknown return type from rdflib.query(). Return it unprocessed."
)
return result # type: ignore

if resulttype == "SELECT":
return [tuple(str(v) for v in row) for row in result] # type: ignore
if resulttype == "ASK":
return bool(result)
if resulttype in ("CONSTRUCT", "DESCRIBE"):
return _convert_triples_to_tripper(result)
assert False, "should never be reached" # nosec

def update(self, update_object, **kwargs) -> None:
"""Update triplestore with SPARQL.
Expand Down Expand Up @@ -234,3 +247,26 @@ def namespaces(self) -> dict:
prefix: str(namespace)
for prefix, namespace in self.graph.namespaces()
}


def _convert_triples_to_tripper(triples) -> "Generator[Triple, None, None]":
"""Help function that converts a iterator/generator of rdflib triples
to tripper triples."""
for s, p, o in triples: ### p ylint: disable=not-an-iterable
yield (
(
f"_:{s}"
if isinstance(s, BNode) and not s.startswith("_:")
else str(s)
),
str(p),
(
parse_literal(o)
if isinstance(o, rdflibLiteral)
else (
f"_:{o}"
if isinstance(o, BNode) and not o.startswith("_:")
else str(o)
)
),
)
5 changes: 4 additions & 1 deletion tripper/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ def query(self, query_object: str, **kwargs) -> List[Tuple[str, ...]]:
kwargs: Additional backend-specific keyword arguments.
Returns:
List of tuples of IRIs for each matching row.
The return type depends on type of query:
- SELECT: list of tuples of IRIs for each matching row
- ASK: bool
- CONSTRUCT, DESCRIBE: generator over triples
"""
def update(self, update_object: str, **kwargs):
Expand Down
Loading

0 comments on commit f7488f5

Please sign in to comment.