diff --git a/tests/datadoc/test_dataset.py b/tests/datadoc/test_dataset.py index b80f0e99..75de722a 100644 --- a/tests/datadoc/test_dataset.py +++ b/tests/datadoc/test_dataset.py @@ -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 @@ -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) @@ -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... @@ -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", @@ -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"}, } @@ -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 @@ -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", @@ -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 @@ -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, } @@ -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", @@ -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. @@ -652,37 +683,41 @@ 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", @@ -690,10 +725,8 @@ def test_update_restrictions(): "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", diff --git a/tripper/datadoc/context.py b/tripper/datadoc/context.py index dfee2a0f..832d67c1 100644 --- a/tripper/datadoc/context.py +++ b/tripper/datadoc/context.py @@ -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() diff --git a/tripper/datadoc/dataset.py b/tripper/datadoc/dataset.py index 8442e7c3..cfc64c4c 100644 --- a/tripper/datadoc/dataset.py +++ b/tripper/datadoc/dataset.py @@ -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") @@ -597,6 +599,7 @@ def update_context( }, } ) + return context def infer_restriction_types( @@ -642,7 +645,7 @@ def infer_restriction_types( - "exactly ": exact cardinality restriction - "min ": minimum cardinality restriction - "max ": maximum cardinality restriction - - "value": value restriction + - "value": value restriction (ignored) where `` is a positive integer. @@ -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: @@ -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) @@ -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)