Skip to content

Commit

Permalink
Allowing backends to indicate whether they prefer the sparql interface (
Browse files Browse the repository at this point in the history
#255)

Allow backends to add a class attribute `prefer_sparql` attribute,
indicating whether they prefer calls to the SPARQL `query()` method
instead of the `triples()` method.

Added the `Triplestore.prefer_sparql` property for easy access to this
attribute.

Also renamed TriplestoreError to TripperError and some additional
cleanup of warnings.

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

---------

Co-authored-by: Francesca L. Bleken <[email protected]>
  • Loading branch information
jesper-friis and francescalb authored Oct 15, 2024
1 parent 455cd08 commit d45e9e6
Show file tree
Hide file tree
Showing 15 changed files with 85 additions and 18 deletions.
13 changes: 12 additions & 1 deletion docs/developers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
# For developers

## New release
## Adding new backends

See [interface.py], which defines the interface of a backend and may serve as a template for creating new backends.



## Creating new release

To create a new release, it is good to have a release summary.

Expand All @@ -15,3 +21,8 @@ Then, go to [create a new GitHub releases](https://github.com/EMMC-ASBL/tripper/
Add again the tag as the release title (optionally write something else that defines this release as a title).

Finally, press the "Publish release" button and ensure the release workflow succeeds (check [the release workflow](https://github.com/EMMC-ASBL/tripper/actions/workflows/cd_release.yml)).




[interface.py]: https://github.com/EMMC-ASBL/tripper/blob/master/tripper/interface.py
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ dependencies = [

[project.optional-dependencies]
pre-commit = [
"pre-commit==2.21.0",
"pre-commit==4.0.1",
"pylint==2.15.5",
]
testing-core = [
Expand Down Expand Up @@ -139,4 +139,5 @@ minversion = "7.0"
addopts = "-rs --cov=tripper --cov-report=term --doctest-modules"
filterwarnings = [
"ignore:.*imp module.*:DeprecationWarning",
"ignore:::tripper.literal:243", # Ignore warning in doctest
]
2 changes: 2 additions & 0 deletions tests/backends/test_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,5 @@ def test_collection():
label = ts3.value(STRUCTURE.name, DM.hasLabel)
assert isinstance(label, Literal)
assert label == Literal("Strontium titanate", lang="en")

assert ts.prefer_sparql is False
2 changes: 2 additions & 0 deletions tests/backends/test_rdflib.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,5 @@ def test_rdflib_backend():
# Test for bnode
ts.add_triples([("_:bn1", RDFS.subClassOf, EX.s)])
assert ts.value(predicate=RDFS.subClassOf, object=EX.s) == "_:bn1"

assert ts.prefer_sparql is False
6 changes: 4 additions & 2 deletions tests/test_literals.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ def test_untyped() -> None:
"""Test creating a untyped literal."""
import pytest

from tripper.errors import UnknownDatatypeWarning
from tripper.literal import XSD, Literal

literal = Literal("Hello world!")
Expand All @@ -23,7 +24,7 @@ def test_untyped() -> None:
# 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, match="^unknown datatype"):
with pytest.warns(UnknownDatatypeWarning, match="^unknown datatype"):
assert literal != Literal("Hello world!", datatype=XSD.ENTITY)


Expand Down Expand Up @@ -227,6 +228,7 @@ def test_parse_literal() -> None:
import pytest

from tripper import RDF, XSD, Literal
from tripper.errors import UnknownDatatypeWarning
from tripper.utils import parse_literal

literal = parse_literal(Literal("abc").n3())
Expand Down Expand Up @@ -315,7 +317,7 @@ def test_parse_literal() -> None:
assert literal.lang is None
assert literal.datatype == RDF.JSON

with pytest.warns(UserWarning, match="unknown datatype"):
with pytest.warns(UnknownDatatypeWarning, match="unknown datatype"):
literal = parse_literal('"value"^^http://example.com/vocab#mytype')
assert literal.value == "value"
assert literal.lang is None
Expand Down
12 changes: 11 additions & 1 deletion tests/test_triplestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ def test_triplestore( # pylint: disable=too-many-locals
# ) == example_function.__doc__
assert ts.value(func_iri, DCTERMS.description, lang="de") is None

# Test the `prefer_sparql` property
facit = {
"collection": False,
"ontopy": False,
"rdflib": False,
"sparqlwrapper": True,
}
assert ts.prefer_sparql == facit[backend]


# if True:
def test_restriction() -> None: # pylint: disable=too-many-statements
Expand Down Expand Up @@ -408,6 +417,7 @@ def test_backend_ontopy(get_ontology_path: "Callable[[str], Path]") -> None:
def test_backend_sparqlwrapper() -> None:
"""Specifically test the SPARQLWrapper backend Triplestore."""
from tripper import SKOS, Triplestore
from tripper.errors import UnknownDatatypeWarning

pytest.importorskip("SPARQLWrapper")
ts = Triplestore(
Expand All @@ -416,7 +426,7 @@ def test_backend_sparqlwrapper() -> None:
"csiro_international-chronostratigraphic-chart_geologic-"
"time-scale-2020",
)
with pytest.warns(UserWarning, match="unknown datatype"):
with pytest.warns(UnknownDatatypeWarning, match="unknown datatype"):
for s, p, o in ts.triples(predicate=SKOS.notation):
assert s
assert p
Expand Down
2 changes: 2 additions & 0 deletions tripper/backends/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ class CollectionStrategy:
triplestore from.
"""

prefer_sparql = False

def __init__(
self,
base_iri: "Optional[str]" = None,
Expand Down
2 changes: 2 additions & 0 deletions tripper/backends/ontopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ class OntopyStrategy:
Either the `base_iri` or `onto` argument must be provided.
"""

prefer_sparql = False

def __init__(
self,
base_iri: "Optional[str]" = None,
Expand Down
2 changes: 2 additions & 0 deletions tripper/backends/rdflib.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ class RdflibStrategy:
creating a new empty Graph object.
"""

prefer_sparql = False

def __init__(
self,
base_iri: "Optional[str]" = None, # pylint: disable=unused-argument
Expand Down
2 changes: 2 additions & 0 deletions tripper/backends/sparqlwrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ class SparqlwrapperStrategy:
"""

prefer_sparql = True

def __init__(self, base_iri: str, **kwargs) -> None:
kwargs.pop(
"database", None
Expand Down
24 changes: 16 additions & 8 deletions tripper/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,34 +2,42 @@


# === Exceptions ===
class TriplestoreError(Exception):
"""Base exception for triplestore errors."""
class TripperError(Exception):
"""Base exception for tripper errors."""


class UniquenessError(TriplestoreError):
class UniquenessError(TripperError):
"""More than one matching triple."""


class NamespaceError(TriplestoreError):
class NamespaceError(TripperError):
"""Namespace error."""


class NoSuchIRIError(NamespaceError):
"""Namespace has no such IRI."""


class CannotGetFunctionError(TriplestoreError):
class CannotGetFunctionError(TripperError):
"""Not able to get function documented in the triplestore."""


class ArgumentTypeError(TriplestoreError, TypeError):
class ArgumentTypeError(TripperError, TypeError):
"""Invalid argument type."""


class ArgumentValueError(TriplestoreError, ValueError):
class ArgumentValueError(TripperError, ValueError):
"""Invalid argument value (of correct type)."""


# === Warnings ===
class UnusedArgumentWarning(Warning):
class TripperWarning(Warning):
"""Base class for tripper warnings."""


class UnusedArgumentWarning(TripperWarning):
"""Argument is unused."""


class UnknownDatatypeWarning(TripperWarning):
"""Unknown datatype."""
4 changes: 4 additions & 0 deletions tripper/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ class ITriplestore(Protocol):
```python
# Whether the backend perfers SPQRQL queries instead of using the
# triples() method.
prefer_sparql = True
def parse(
self,
source: Union[str, Path, IO] = None,
Expand Down
4 changes: 3 additions & 1 deletion tripper/literal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
from datetime import datetime
from typing import TYPE_CHECKING

from tripper.errors import UnknownDatatypeWarning
from tripper.namespace import RDF, RDFS, XSD

if TYPE_CHECKING: # pragma: no cover
Expand Down Expand Up @@ -240,7 +241,8 @@ def __new__(
string.datatype in types for types in cls.datatypes.values()
):
warnings.warn(
f"unknown datatype: {string.datatype} - assuming xsd:string"
f"unknown datatype: {string.datatype} - assuming xsd:string",
category=UnknownDatatypeWarning,
)

return string
Expand Down
21 changes: 19 additions & 2 deletions tripper/triplestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
ArgumentValueError,
CannotGetFunctionError,
NamespaceError,
TriplestoreError,
TripperError,
UniquenessError,
)
from tripper.literal import Literal
Expand Down Expand Up @@ -539,6 +539,23 @@ def list_databases(cls, backend: str, **kwargs):
# interfaces to the triples(), add_triples() and remove() methods
# implemented by all backends.

prefer_sparql = property(
fget=lambda self: getattr(self.backend, "prefer_sparql", None),
doc=(
"Whether the backend prefer SPARQL over the triples() interface. "
"Is None if not specified by the backend."
"\n\n"
"Even though Tripper requires that the Triplestore.triples() is "
"implemented, Triplestore.query() must be used for some "
"backends in specific cases (like fuseki when working on RDF "
"lists) because of how blank nodes are treated. "
"\n\n"
"The purpose of this property is to let tripper "
"automatically select the most appropriate interface depending "
"on the current backend settings."
),
)

@classmethod
def _get_backend(cls, backend: str, package: "Optional[str]" = None):
"""Returns the class implementing the given backend."""
Expand Down Expand Up @@ -1013,7 +1030,7 @@ def add_mapsTo(
self.bind("map", MAP)

if not property_name and not isinstance(source, str):
raise TriplestoreError(
raise TripperError(
"`property_name` is required when `target` is not a string."
)

Expand Down
4 changes: 2 additions & 2 deletions tripper/tripper.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import uuid
from typing import TYPE_CHECKING

from tripper.errors import TriplestoreError
from tripper.errors import TripperError
from tripper.literal import Literal
from tripper.namespace import MAP, RDF
from tripper.triplestore import (
Expand Down Expand Up @@ -186,7 +186,7 @@ def get_value(
)
return retval

raise TriplestoreError(
raise TripperError(
f"data source {iri} has neither a 'hasDataValue' or a "
f"'hasAccessFunction' property"
)
Expand Down

0 comments on commit d45e9e6

Please sign in to comment.