Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
25beddf
initial work
jesper-friis Feb 11, 2026
fc0f875
Merge branch 'master' into individual-restrictions
jesper-friis Feb 12, 2026
41c7920
Generalised update_classes() to update_restrictions()
jesper-friis Feb 15, 2026
30d7f01
Corrected other tests due increased validation
jesper-friis Feb 15, 2026
78f3a7e
Do not try to expand a blank node
jesper-friis Feb 15, 2026
2c20884
Skip test_fetch() if dlite is not available
jesper-friis Feb 15, 2026
658a321
Merge branch 'master' into individual-restrictions
jesper-friis Feb 16, 2026
9e43e9f
Added comments to tests and made them more sensible
jesper-friis Feb 18, 2026
f4990ac
Apply suggestion from @francescalb
francescalb Feb 18, 2026
0f60a7e
Update tests/datadoc/test_dataset.py
jesper-friis Feb 19, 2026
44ffd56
Update tripper/datadoc/dataset.py
jesper-friis Feb 19, 2026
bc02576
Removed too broad pytest.importorskip()
jesper-friis Feb 19, 2026
fbde245
Made namespace expansion of @id forgiving
jesper-friis Feb 19, 2026
5c6f19e
Skip test depending on test_sparqlwrapper() if sparqlwrapper is not a…
jesper-friis Feb 19, 2026
ff443be
Added support for multiple labels in keywords
jesper-friis Feb 26, 2026
dcb1e08
Remove duplicated labels in keyworkds
jesper-friis Feb 26, 2026
be46b4d
Allow duplicated labels
jesper-friis Feb 26, 2026
2f1db7c
Do not include blank nodes when testing for klasses in keywords
francescalb Mar 2, 2026
034423b
Merge branch 'master' into multiple-labels
francescalb Mar 2, 2026
2d2a930
Apply suggestion from @francescalb
francescalb Mar 2, 2026
79b7ca8
Allow epansion on individuals (not in context) when expanding restric…
francescalb Mar 3, 2026
dc1264f
Added new function: update_context()
jesper-friis Mar 3, 2026
6138d7b
Updated tests
jesper-friis Mar 3, 2026
60a331d
Merge branch 'multiple-labels' of github.com:EMMC-ASBL/tripper into m…
jesper-friis Mar 3, 2026
89981df
latest state
jesper-friis Mar 9, 2026
cef25d6
Merge branch 'master' into keywords-with-context-input
jesper-friis Mar 9, 2026
d6615fc
cleanup
jesper-friis Mar 9, 2026
12cb61a
Merge remote-tracking branch 'origin/restrictions_to_iris' into keywo…
jesper-friis Mar 9, 2026
328861f
Updated test
jesper-friis Mar 9, 2026
636c4bd
Fixed expansion of subPropertyOf
jesper-friis Mar 9, 2026
f2e53be
Fixed test_infer_restriction_types()
jesper-friis Mar 9, 2026
1ef9b39
Merge branch 'master' into keywords-with-context-input
jesper-friis Mar 9, 2026
6b853d1
Merge branch 'keywords-with-context-input' into individual-restrictions
jesper-friis Mar 9, 2026
a4556e2
Added missing tests
jesper-friis Mar 10, 2026
583992d
Merge branch 'master' into keywords-with-context-input
jesper-friis Mar 10, 2026
5b51fb5
Added more tests and ignored some warnings
jesper-friis Mar 10, 2026
73e5768
Merge branch 'keywords-with-context-input' into individual-restrictions
jesper-friis Mar 11, 2026
fb4512b
commit during work-in-progress
jesper-friis Mar 11, 2026
38c17fd
Moved configurations for ignoring warnings to pyproject.toml
jesper-friis Mar 11, 2026
e307f5c
Cleaned up some additional warnings
jesper-friis Mar 11, 2026
4964a45
Merge branch 'keywords-with-context-input' into individual-restrictions
jesper-friis Mar 11, 2026
fcc36c7
Updated update_restrictions()
jesper-friis Mar 12, 2026
0e98269
Fixed restrictions to classes
jesper-friis Mar 12, 2026
9bbc132
Merge branch 'master' into individual-restrictions
jesper-friis Mar 12, 2026
8f6d32a
Added test for update_context() with full IRIs
jesper-friis Mar 12, 2026
964d97f
Merge branch 'master' into individual-restrictions
francescalb Mar 16, 2026
98bca9d
Merge branch 'master' into individual-restrictions
jesper-friis Apr 9, 2026
ef05772
Added return value to update_context().
jesper-friis Apr 9, 2026
c1cc808
Merge branch 'master' into individual-restrictions
francescalb Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 62 additions & 29 deletions tests/datadoc/test_dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ def test_store():

def test_update_context():
"""Test update_context()."""
from tripper import HUME, OWL, Namespace
from tripper import HUME, OWL, SKOS, Namespace
from tripper.datadoc import get_context
from tripper.datadoc.dataset import update_context

Expand All @@ -425,10 +425,18 @@ def test_update_context():
"@id": "ex:instr2",
},
{
# Add both ex:MyDevice and hume:Device to context
"@id": "ex:MyDevice",
"skos:prefLabel": "MyDevice",
"subClassOf": "hume:Device",
},
{
# Check for full IRI
"@id": EX.MyClass,
"@type": OWL.Class,
SKOS.prefLabel: "MyClass",
HUME.hasPart: EX.MyDevice,
},
],
}
context = get_context(default_theme=None)
Expand All @@ -439,6 +447,7 @@ def test_update_context():
assert "MyDevice" in c
assert c["MyDevice"] == {"@id": EX.MyDevice, "@type": OWL.Class}
assert c["Device"] == {"@id": HUME.Device, "@type": OWL.Class}
assert c["MyClass"] == {"@id": EX.MyClass, "@type": OWL.Class}

# TODO: add tests for what happens if there is mismatch between
# previously added context and updated_context...
Expand Down Expand Up @@ -490,6 +499,12 @@ def test_infer_restriction_types():
"@id": "ex:instr2",
"isDefinedBy": HUME.MeasuringInstrument,
},
{
# An individial relating to two classes and an individual.
"@id": "ex:instr3",
"@type": HUME.Device,
"hasPart": [HUME.MeasuringInstrument, "MyDevice", "ex:instr"],
},
{
"@id": "ex:MyDevice",
# "@type": "owl:Class",
Expand All @@ -500,6 +515,7 @@ def test_infer_restriction_types():
}
assert infer_restriction_types(sources, ctx) == {
EX.instr2: {RDFS.isDefinedBy: "some"},
EX.instr3: {DCTERMS.hasPart: "some"},
EX.MyDevice: {DCTERMS.hasPart: "some"},
}

Expand All @@ -508,13 +524,15 @@ def test_update_restrictions():
"""Test update_restrictions()."""
from copy import deepcopy

from tripper import DCAT, HUME
from tripper import HUME, Namespace
from tripper.datadoc import get_context
from tripper.datadoc.dataset import (
infer_restriction_types,
update_context,
update_restrictions,
)

EX = Namespace("http://example.org#")
ctx = get_context()

# Just a data property - nothing to update
Expand Down Expand Up @@ -542,7 +560,8 @@ def test_update_restrictions():
assert "dcat:Dataset" in r2["subClassOf"]
assert {
"@type": "owl:Restriction",
"owl:onProperty": {"@id": DCAT.distribution},
# "owl:onProperty": {"@id": DCAT.distribution}, # Unnessesary nesting!
"owl:onProperty": "dcat:distribution",
"owl:hasValue": {
"@type": "dcat:Distribution",
"accessService": "ex:service",
Expand All @@ -565,8 +584,10 @@ def test_update_restrictions():
update_restrictions(r3, ctx)
assert r3["subClassOf"] == {
"@type": "owl:Restriction",
"owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
"owl:someValuesFrom": {"@id": "http://example.com/ex#Wheel"},
# "owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
# "owl:someValuesFrom": {"@id": "http://example.com/ex#Wheel"},
"owl:onProperty": "dcterms:hasPart",
"owl:someValuesFrom": "ex:Wheel",
}

# Now, use the restriction argument to specify that we should convert
Expand All @@ -578,8 +599,10 @@ def test_update_restrictions():
update_restrictions(r4, ctx, restrictions=restrictions)
assert r4["subClassOf"] == {
"@type": "owl:Restriction",
"owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
"owl:onClass": {"@id": "http://example.com/ex#Wheel"},
# "owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
# "owl:onClass": {"@id": "http://example.com/ex#Wheel"},
"owl:onProperty": "dcterms:hasPart",
"owl:onClass": "ex:Wheel",
"owl:qualifiedCardinality": 1,
}

Expand All @@ -590,13 +613,17 @@ def test_update_restrictions():
update_restrictions(r4, ctx, restrictions=restrictions)
assert r4["subClassOf"] == {
"@type": "owl:Restriction",
"owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
"owl:onClass": {"@id": "http://example.com/ex#Wheel"},
# "owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
# "owl:onClass": {"@id": "http://example.com/ex#Wheel"},
"owl:onProperty": "dcterms:hasPart",
"owl:onClass": "ex:Wheel",
"owl:qualifiedCardinality": 1,
}

d6 = {
"@context": {
"ex": str(EX),
"hume": str(HUME),
"MeasuringInstrument": {
"@id": HUME.MeasuringInstrument,
"@type": "owl:Class",
Expand Down Expand Up @@ -624,7 +651,11 @@ def test_update_restrictions():
# Should be converted to an existential restriction.
"@id": "ex:instr3",
"@type": HUME.Device,
"hasPart": [HUME.MeasuringInstrument, "MyDevice", "ex:instr"],
"hasPart": [
HUME.MeasuringInstrument,
"ex:MyDevice",
"ex:instr",
],
},
{
# A class relating to a class.
Expand Down Expand Up @@ -652,48 +683,50 @@ def test_update_restrictions():
],
}
r6 = deepcopy(d6)
update_restrictions(r6, ctx)
c6 = update_context(r6, ctx.copy())
update_restrictions(r6, c6)
res6 = {d["@id"]: d for d in r6["@graph"]}
assert res6["ex:instr"] == {
assert res6["ex:instr"] == { # Expect: no conversion
"@id": "ex:instr",
"@type": "https://w3id.org/emmo/hume#Device",
"isDefinedBy": "https://w3id.org/emmo/hume#MeasuringSystem",
}
assert res6["ex:instr2"] == {
assert res6["ex:instr2"] == { # Expect: existential restriction
"@id": "ex:instr2",
"@type": [
"https://w3id.org/emmo/hume#Device",
{
"@type": "owl:Restriction",
"owl:onProperty": {
"@id": "http://www.w3.org/2000/01/rdf-schema#isDefinedBy",
},
"owl:someValuesFrom": {
"@id": "https://w3id.org/emmo/hume#MeasuringInstrument",
},
"owl:onProperty": "rdfs:isDefinedBy",
"owl:someValuesFrom": "hume:MeasuringInstrument",
},
],
}
assert res6["ex:instr3"] == {
# WRONG! Should be converted to restrictions
"@id": "ex:instr3",
"@type": "https://w3id.org/emmo/hume#Device",
"hasPart": [
"https://w3id.org/emmo/hume#MeasuringInstrument",
"MyDevice",
"ex:instr",
"@type": [
"https://w3id.org/emmo/hume#Device",
{
"@type": "owl:Restriction",
"owl:onProperty": "dcterms:hasPart",
"owl:someValuesFrom": "hume:MeasuringInstrument",
},
{
"@type": "owl:Restriction",
"owl:onProperty": "dcterms:hasPart",
"owl:someValuesFrom": "ex:MyDevice",
},
],
"hasPart": "ex:instr",
}
assert res6["ex:MyDevice"] == {
"@id": "ex:MyDevice",
"subClassOf": [
"https://w3id.org/emmo/hume#Device",
{
"@type": "owl:Restriction",
"owl:onProperty": {"@id": "http://purl.org/dc/terms/hasPart"},
"owl:someValuesFrom": {
"@id": "https://w3id.org/emmo/hume#MeasuringInstrument"
},
"owl:onProperty": "dcterms:hasPart",
"owl:someValuesFrom": "hume:MeasuringInstrument",
},
],
"label": "MyDevice",
Expand Down
8 changes: 7 additions & 1 deletion tripper/datadoc/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,13 @@ def rec(dct):
d[k] = rec(v)
return d

self.ctx = self.ld.process_context(self.ctx, rec(context), options={})
try:
self.ctx = self.ld.process_context(
self.ctx, rec(context), options={}
)
except jsonld.JsonLdError: # pylint: disable=try-except-raise
# TODO: convert error message to something more readable
raise

# Clear caches
self._expanded.clear()
Expand Down
75 changes: 38 additions & 37 deletions tripper/datadoc/dataset.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,10 +551,12 @@ def _isclass(d, context):
def update_context(
source: "Union[dict, list]",
context: "Context",
) -> None:
) -> "Context":
"""Update `context` with information from `source`.

Currently this only adds classes defined in `source` to `context`.

Returns the updated context.
"""
subclassof = (RDFS.subClassOf, "rdfs:subClassOf", "subClassOf")

Expand Down Expand Up @@ -597,6 +599,7 @@ def update_context(
},
}
)
return context


def infer_restriction_types(
Expand Down Expand Up @@ -642,7 +645,7 @@ def infer_restriction_types(
- "exactly <N>": exact cardinality restriction
- "min <N>": minimum cardinality restriction
- "max <N>": maximum cardinality restriction
- "value": value restriction
- "value": value restriction (ignored)

where `<N>` is a positive integer.

Expand Down Expand Up @@ -699,6 +702,9 @@ def infer_restriction_types(
elif isinstance(v, list):
if any(_isclass(e, context) for e in v):
d[kexp] = "some"
elif isinstance(v, list):
if any(_isclass(e, context) for e in v):
d[kexp] = "some"
elif _isclass(v, context):
d[kexp] = "some"
if d:
Expand Down Expand Up @@ -730,24 +736,24 @@ def update_restrictions(
"""
# pylint: disable=too-many-statements

def asiri(v, strict=False):
"""Return `v` as prefixed IRI(s)."""
if isinstance(v, str):
return context.prefixed(v, strict=strict)
if isinstance(v, list):
return [asiri(e, strict=strict) for e in v]
return v

def addrestriction(source, prop, value):
"""Add restriction to `source`."""
# pylint: disable=no-else-return

def as_iri_node(v):
"""Return JSON-LD node object for IRI-like string values."""
if isinstance(v, str) and is_uri(v, require_netloc=False):
return {"@id": context.expand(v, strict=False)}
return v

iri = context.expand(source["@id"]) if "@id" in source else "*"
propiri = context.expand(prop)
if value is None or prop.startswith("@"):
return
elif isinstance(value, dict):
update_restrictions(
value, context=context, restrictions=restrictions
)
update_restrictions(value, context, restrictions)
elif isinstance(value, list):
for val in value:
addrestriction(source, prop, val)
Expand All @@ -765,37 +771,32 @@ def as_iri_node(v):
d = {
"@type": "owl:Restriction",
# We expand here, since JSON-LD doesn't expand values.
"owl:onProperty": {
"@id": context.expand(prop, strict=True),
},
"owl:onProperty": asiri(prop, strict=True),
}
if restrictionType == "value":
d["owl:hasValue"] = as_iri_node(value)
elif restrictionType == "some":
d["owl:someValuesFrom"] = as_iri_node(value)
elif restrictionType == "only":
d["owl:allValuesFrom"] = as_iri_node(value)
else:
d["owl:onClass"] = as_iri_node(value)
ctype, n = restrictionType.split()
ctypes = {
"exactly": "owl:qualifiedCardinality",
"min": "owl:minQualifiedCardinality",
"max": "owl:maxQualifiedCardinality",
}
d[ctypes[ctype]] = int(n)

if _isclassdoc(source):
add(source, "subClassOf", d)
if _isclass(value, context):
if restrictionType == "only":
d["owl:allValuesFrom"] = asiri(value)
elif restrictionType.startswith(("exactly", "min", "max")):
d["owl:onClass"] = asiri(value)
ctype, n = restrictionType.split()
ctypes = {
"exactly": "owl:qualifiedCardinality",
"min": "owl:minQualifiedCardinality",
"max": "owl:maxQualifiedCardinality",
}
d[ctypes[ctype]] = int(n)
else: # default: some
d["owl:someValuesFrom"] = asiri(value)
elif _isclass(source, context):
d["owl:hasValue"] = asiri(value)
else:
add(source, "@type", d)
add(source, prop, value)
return

# Replace source[prop] with restriction
if prop in source: # Avoid removing prop more than once
del source[prop]

# Recursively update related calsses
if restrictionType != "value" and isinstance(value, dict):
update_restrictions(value, context, restrictions)
add(source, "subClassOf" if _isclassdoc(source) else "@type", d)

# Local context
context = get_context(context=context)
Expand Down
Loading