Skip to content

Commit

Permalink
Added option to Triplestore.value() to return a generator over all ma…
Browse files Browse the repository at this point in the history
…tching values (#252)

# Description
Added option to Triplestore.value() to return a generator over all
matching values. Also added more tests.

**Question**: Is there a better way to indicate that we should return
all matches? Using `any=None` to indicate that is not very intuitive.
But using a special value of the `any` argument is logical, since
returning any match (`any=True`), returning all matches (`any=None`) and
require only one match (`any=False`) are mutually exclusive.

> How about using matches="all", "any", "unique" and have unique as default? I suppose it needs to be added in addition to any, to be deprecated.

The above suggestion can be a new PR

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

Co-authored-by: Francesca L. Bleken <48128015+francescalb@users.noreply.github.com>
  • Loading branch information
jesper-friis and francescalb authored Sep 26, 2024
1 parent 07c9d82 commit 9bf410b
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 23 deletions.
66 changes: 66 additions & 0 deletions tests/test_triplestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -512,3 +512,69 @@ def test_bind_errors():
ts2.bind("ex")
with pytest.raises(TypeError):
ts2.bind("ex", Ellipsis)


def test_value():
"""Test Triplestore.value()."""
pytest.importorskip("rdflib")

from tripper import DCTERMS, RDF, RDFS, Literal, Triplestore
from tripper.errors import UniquenessError

ts = Triplestore(backend="rdflib")
EX = ts.bind("ex", "http://example.com#")
l1 = Literal("First comment...")
l2en = Literal("Second comment...", lang="en")
l2da = Literal("Anden kommentar...", lang="da")
l3en = Literal("Third comment...", lang="en")
ts.add_triples(
[
(EX.mydata, RDF.type, EX.Dataset),
(EX.mydata, DCTERMS.title, Literal("My little data")),
(EX.mydata, RDFS.comment, l1),
(EX.mydata, RDFS.comment, l2en),
(EX.mydata, RDFS.comment, l2da),
(EX.mydata, RDFS.comment, l3en),
]
)

assert ts.value(subject=EX.mydata, predicate=RDF.type) == EX.Dataset
assert ts.value(predicate=RDF.type, object=EX.Dataset) == EX.mydata
assert ts.value(subject=EX.mydata, predicate=DCTERMS.title) == Literal(
"My little data"
)

with pytest.raises(UniquenessError):
ts.value(subject=EX.mydata, predicate=RDFS.comment, lang="en")

assert (
ts.value(subject=EX.mydata, predicate=RDFS.comment, lang="da") == l2da
)

assert ts.value(
subject=EX.mydata,
predicate=RDFS.comment,
lang="en",
any=True,
) in (l2en, l3en)

assert set(
ts.value(
subject=EX.mydata,
predicate=RDFS.comment,
lang="en",
any=None,
)
) == {l2en, l3en}

assert (
ts.value(subject=EX.mydata, predicate=RDFS.comment, lang="no") is None
)

t = ts.value(
subject=EX.mydata,
predicate=RDFS.comment,
lang="no",
default="a",
)
assert t == "a"
46 changes: 23 additions & 23 deletions tripper/triplestore.py
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,11 @@ def value( # pylint: disable=redefined-builtin
predicate: Possible criteria to match.
object: Possible criteria to match.
default: Value to return if no matches are found.
any: If true, return any matching value, otherwise raise
UniquenessError.
any: Used to define how many values to return. Can be set to:
`False` (default): return the value or raise UniquenessError
if there is more than one matching value.
`True`: return any matching value if there is more than one.
`None`: return a generator over all matching values.
lang: If provided, require that the value must be a localised
literal with the given language code.
Expand All @@ -605,33 +608,30 @@ def value( # pylint: disable=redefined-builtin
(idx,) = [i for i, v in enumerate(spo) if v is None]

triples = self.triples(subject, predicate, object)

if lang:
first = None
if idx != 2:
raise ValueError("`object` must be None if `lang` is given")
for triple in triples:
value = triple[idx]
if isinstance(value, Literal) and value.lang == lang:
if any:
return value
if first:
raise UniquenessError("More than one match")
first = value
if first is None:
return default
else:
try:
triple = next(triples)
except StopIteration:
return default
triples = (
t
for t in triples
if isinstance(t[idx], Literal)
and t[idx].lang == lang # type: ignore
)

if any is None:
return (t[idx] for t in triples) # type: ignore

try:
value = next(triples)[idx]
except StopIteration:
return default

try:
next(triples)
except StopIteration:
return triple[idx]
return value

if any:
return triple[idx]
if any is True:
return value
raise UniquenessError("More than one match")

def subjects(
Expand Down

0 comments on commit 9bf410b

Please sign in to comment.