diff --git a/tests/test_triplestore.py b/tests/test_triplestore.py index 51648503..27548d97 100644 --- a/tests/test_triplestore.py +++ b/tests/test_triplestore.py @@ -56,6 +56,23 @@ def test_triplestore( ts_as_turtle = store.serialize(format="turtle") assert ts_as_turtle == expected_function_triplestore + # Test SPARQL query + rows = store.query("SELECT ?s ?o WHERE { ?s rdfs:subClassOf ?o }") + assert len(rows) == 3 + rows.sort() # ensure consistent ordering of rows + assert rows[0] == ( + "http://example.com/onto#AnotherConcept", + "http://www.w3.org/2002/07/owl#Thing", + ) + assert rows[1] == ( + "http://example.com/onto#MyConcept", + "http://www.w3.org/2002/07/owl#Thing", + ) + assert rows[2] == ( + "http://example.com/onto#Sum", + "http://www.w3.org/2002/07/owl#Thing", + ) + def test_backend_rdflib(expected_function_triplestore: str) -> None: """Specifically test the rdflib backend Triplestore. diff --git a/tripper/backends/ontopy.py b/tripper/backends/ontopy.py index 90a0aab0..bbb8efbb 100644 --- a/tripper/backends/ontopy.py +++ b/tripper/backends/ontopy.py @@ -14,7 +14,9 @@ if TYPE_CHECKING: # pragma: no cover from collections.abc import Sequence - from typing import Generator, Optional, Union + from typing import Generator, List, Optional, Union + + from rdflib.query import Result from tripper.triplestore import Triple @@ -202,8 +204,21 @@ def serialize( os.remove(filename) return None - def query(self, query_object, native=True, **kwargs): - """SPARQL query.""" + def query(self, query_object, native=True, **kwargs) -> "Union[List, Result]": + """SPARQL query. + + Parameters: + query_object: String with the SPARQL query. + native: Whether or not to use EMMOntoPy/Owlready2 or RDFLib. + kwargs: Keyword arguments passed to rdflib.Graph.query(). + + Returns: + SPARQL query results. + + """ + if TYPE_CHECKING: + res: "Union[List, Result]" + if native: res = self.onto.world.sparql(query_object) else: @@ -212,8 +227,19 @@ def query(self, query_object, native=True, **kwargs): # TODO: Convert result to expected type return res - def update(self, update_object, native=True, **kwargs): - """Update triplestore with SPARQL.""" + def update(self, update_object, native=True, **kwargs) -> None: + """Update triplestore with SPARQL. + + Parameters: + update_object: String with the SPARQL query. + native: Whether or not to use EMMOntoPy/Owlready2 or RDFLib. + kwargs: Keyword arguments passed to rdflib.Graph.update(). + + Note: + This method is intended for INSERT and DELETE queries. Use + the query() method for SELECT queries. + + """ if native: self.onto.world.sparql(update_object) else: diff --git a/tripper/backends/rdflib.py b/tripper/backends/rdflib.py index 083e5d60..17af52ce 100644 --- a/tripper/backends/rdflib.py +++ b/tripper/backends/rdflib.py @@ -16,7 +16,7 @@ if TYPE_CHECKING: # pragma: no cover from collections.abc import Sequence - from typing import Generator, Union + from typing import Generator, List, Tuple, Union from tripper.triplestore import Triple @@ -140,13 +140,32 @@ def serialize( return result if isinstance(result, str) else result.decode() return None - def query(self, query_object, **kwargs): - """SPARQL query.""" - # TODO: convert to returned object - return self.graph.query(query_object=query_object, **kwargs) + def query(self, query_object, **kwargs) -> "List[Tuple[str, ...]]": + """SPARQL query. - def update(self, update_object, **kwargs): - """Update triplestore with SPARQL.""" + 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. + + """ + rows = self.graph.query(query_object=query_object, **kwargs) + return [tuple(str(v) for v in row) for row in rows] + + def update(self, update_object, **kwargs) -> None: + """Update triplestore with SPARQL. + + Parameters: + update_object: String with the SPARQL query. + kwargs: Keyword arguments passed to rdflib.Graph.update(). + + Note: + This method is intended for INSERT and DELETE queries. Use + the query() method for SELECT queries. + + """ return self.graph.update(update_object=update_object, **kwargs) def bind(self, prefix: str, namespace: str): diff --git a/tripper/interface.py b/tripper/interface.py index fcbe4e34..a2a124a9 100644 --- a/tripper/interface.py +++ b/tripper/interface.py @@ -23,15 +23,27 @@ class ITriplestore(Protocol): ```python - def __init__(self, base_iri=None, **kwargs) + def __init__(self, base_iri: str = None, **kwargs): + """Initialise triplestore. - def parse(self, source=None, location=None, data=None, format=None, - **kwargs): + Arguments: + base_iri: Optional base IRI to initiate the triplestore from. + kwargs: Additional keyword arguments passed to the backend. + """ + + def parse( + self, + source: Union[str, Path, IO] = None, + location: str = None, + data: str = None, + format: str = None, + **kwargs + ): """Parse source and add the resulting triples to triplestore. The source is specified using one of `source`, `location` or `data`. - Parameters: + Arguments: source: File-like object or file name. location: String with relative or absolute URL to source. data: String containing the data to be parsed. @@ -40,10 +52,15 @@ def parse(self, source=None, location=None, data=None, format=None, the parsing. """ - def serialize(self, destination=None, format='xml', **kwargs) + def serialize( + self, + destination: Union[str, Path, IO] = None, + format: str ='xml', + **kwargs + ): """Serialise to destination. - Parameters: + Arguments: destination: File name or object to write to. If None, the serialisation is returned. format: Format to serialise as. Supported formats, depends on @@ -55,19 +72,36 @@ def serialize(self, destination=None, format='xml', **kwargs) Serialised string if `destination` is None. """ - def query(self, query_object, **kwargs) - """SPARQL query.""" + def query(self, query_object: str, **kwargs) -> List: + """SPARQL query. + + Arguments: + query_object: String with the SPARQL query. + kwargs: Additional backend-specific keyword arguments. + + Returns: + List of tuples of IRIs for each matching row. + """ + + def update(self, update_object: str, **kwargs): + """Update triplestore with SPARQL. - def update(self, update_object, **kwargs) - """Update triplestore with SPARQL.""" + Arguments: + query_object: String with the SPARQL query. + kwargs: Additional backend-specific keyword arguments. - def bind(self, prefix: str, namespace: str) + Note: + This method is intended for INSERT and DELETE queries. Use + the query() method for SELECT queries. + """ + + def bind(self, prefix: str, namespace: str) -> Namespace: """Bind prefix to namespace. Should only be defined if the backend supports namespaces. """ - def namespaces(self) -> dict + def namespaces(self) -> dict: """Returns a dict mapping prefixes to namespaces. Should only be defined if the backend supports namespaces. @@ -78,10 +112,21 @@ def namespaces(self) -> dict ''' def triples(self, triple: "Triple") -> "Generator": - """Returns a generator over matching triples.""" + """Returns a generator over matching triples. + + Arguments: + triple: A `(s, p, o)` tuple where `s`, `p` and `o` should + either be None (matching anything) or an exact IRI to + match. + """ def add_triples(self, triples: "Sequence[Triple]"): - """Add a sequence of triples.""" + """Add a sequence of triples. + + Arguments: + triples: A sequence of `(s, p, o)` tuples to add to the + triplestore. + """ def remove(self, triple: "Triple"): """Remove all matching triples from the backend.""" diff --git a/tripper/triplestore.py b/tripper/triplestore.py index bb41206a..3d09693d 100644 --- a/tripper/triplestore.py +++ b/tripper/triplestore.py @@ -24,7 +24,9 @@ if TYPE_CHECKING: # pragma: no cover from collections.abc import Mapping - from typing import Any, Callable, Dict, Generator, Tuple, Union + from typing import Any, Callable, Dict, Generator, List, Tuple, Union + + from rdflib.query import Result as rdflibResult Triple = Tuple[Union[str, None], Union[str, None], Union[str, None]] @@ -393,11 +395,22 @@ def __init__(self, backend: str, base_iri: str = None, **kwargs): # Methods implemented by backend # ------------------------------ def triples(self, triple: "Triple") -> "Generator": - """Returns a generator over matching triples.""" + """Returns a generator over matching triples. + + Arguments: + triple: A `(s, p, o)` tuple where `s`, `p` and `o` should + either be None (matching anything) or an exact IRI to + match. + """ return self.backend.triples(triple) def add_triples(self, triples: "Sequence[Triple]"): - """Add a sequence of triples.""" + """Add a sequence of triples. + + Arguments: + triples: A sequence of `(s, p, o)` tuples to add to the + triplestore. + """ self.backend.add_triples(triples) def remove(self, triple: "Triple"): @@ -458,13 +471,38 @@ def serialize( self._check_method("serialize") return self.backend.serialize(destination=destination, format=format, **kwargs) - def query(self, query_object, **kwargs): - """SPARQL query.""" + def query( + self, query_object, **kwargs + ) -> "Union[rdflibResult, List, List[Tuple[str, ...]]]": + """SPARQL query. + + Parameters: + query_object: String with the SPARQL query. + kwargs: Keyword arguments passed to the backend query() method. + + Returns: + List of tuples of IRIs for each matching row. + + Note: + This method is intended for SELECT queries. Use + the update() method for INSERT and DELETE queries. + + """ self._check_method("query") return self.backend.query(query_object=query_object, **kwargs) - def update(self, update_object, **kwargs): - """Update triplestore with SPARQL.""" + def update(self, update_object, **kwargs) -> None: + """Update triplestore with SPARQL. + + Parameters: + update_object: String with the SPARQL query. + kwargs: Keyword arguments passed to the backend update() method. + + Note: + This method is intended for INSERT and DELETE queries. Use + the query() method for SELECT queries. + + """ self._check_method("update") return self.backend.update(update_object=update_object, **kwargs)