Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
34 changes: 34 additions & 0 deletions docs/generators/owl.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,40 @@ Mapping

.. note:: The current default settings for ``metaclasses`` and ``type-objects`` may change in the future

Metadata values that are IRIs
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

Schema metadata and ``annotations`` become annotation triples on the ontology, class or
property. Their values are literals, except for properties whose value is a resource by
their own specification: the OWL ontology properties ``owl:versionIRI``,
``owl:priorVersion``, ``owl:backwardCompatibleWith`` and ``owl:incompatibleWith``;
``rdfs:seeAlso`` and ``rdfs:isDefinedBy``; the DCMI terms that relate a resource to
another (``dcterms:license``, ``dcterms:conformsTo``, ``dcterms:references``,
``dcterms:source``, ``dcterms:relation`` and its sub-properties); and
``prov:wasDerivedFrom``, ``prov:wasRevisionOf`` and ``prov:hadPrimarySource``.

A value of one of these is emitted as an IRI when it is an absolute IRI or a CURIE with a
declared prefix; any other value stays a literal:

.. code-block:: yaml

id: https://example.org/onto/v2
license: https://www.eclipse.org/legal/epl-2.0/
annotations:
owl:versionInfo: v2
owl:priorVersion: ex:onto/v1
prov:wasDerivedFrom: https://example.org/releases/tag/v2.0.0

.. code-block:: turtle

<https://example.org/onto/v2> a owl:Ontology ;
owl:versionInfo "v2" ;
owl:priorVersion <https://example.org/onto/v1> ;
prov:wasDerivedFrom <https://example.org/releases/tag/v2.0.0> ;
dcterms:license <https://www.eclipse.org/legal/epl-2.0/> .

``dcterms:identifier`` stays a literal: DCMI gives it the range ``rdfs:Literal``.

Enums and PermissibleValues
^^^^^^^^^^^^^^^^^^^^^^^^^^^

Expand Down
68 changes: 67 additions & 1 deletion packages/linkml/src/linkml/generators/owlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import logging
import os
import re
from collections import defaultdict
from collections.abc import Iterable, Sequence
from copy import copy
Expand Down Expand Up @@ -54,6 +55,49 @@

SWRL = rdflib.Namespace("http://www.w3.org/2003/11/swrl#")
SWRLB = rdflib.Namespace("http://www.w3.org/2003/11/swrlb#")
PROV = rdflib.Namespace("http://www.w3.org/ns/prov#")

IRI_VALUED_METADATA_PROPERTIES: frozenset[URIRef] = frozenset(
{
# OWL 2 ontology properties and the version IRI relate an ontology to other
# ontologies (OWL 2 Structural Specification, sections 3.1 and 3.5).
OWL.versionIRI,
OWL.priorVersion,
OWL.backwardCompatibleWith,
OWL.incompatibleWith,
# rdfs:range rdfs:Resource, defined as pointing at a resource (RDF Schema 1.1, 5.4).
RDFS.seeAlso,
RDFS.isDefinedBy,
# DCMI Metadata Terms relating a resource to another resource.
DCTERMS.license,
DCTERMS.conformsTo,
DCTERMS.relation,
DCTERMS.references,
DCTERMS.isReferencedBy,
DCTERMS.hasVersion,
DCTERMS.isVersionOf,
DCTERMS.replaces,
DCTERMS.isReplacedBy,
DCTERMS.requires,
DCTERMS.isRequiredBy,
DCTERMS.source,
# rdfs:range prov:Entity (PROV-O).
PROV.wasDerivedFrom,
PROV.wasRevisionOf,
PROV.hadPrimarySource,
}
)
"""Metadata properties whose value is a resource by their own specification.

A value of one of these properties that is an IRI, or a CURIE with a declared prefix, is
emitted as an IRI node; any other value stays a literal. ``dcterms:identifier`` is not
listed: DCMI gives it the range ``rdfs:Literal``.
"""

# An absolute IRI: an RFC 3986 scheme, a colon, and no character an IRI cannot contain
# (RFC 3987, section 2.2). A CURIE has the same shape, so it is checked against the
# declared prefixes first.
_ABSOLUTE_IRI = re.compile(r"^[A-Za-z][A-Za-z0-9+.\-]*:[^\s<>\"{}|\\^`]+$")


@unique
Expand Down Expand Up @@ -396,6 +440,9 @@ def add_metadata(self, e: Definition | PermissibleValue, uri: URIRef) -> None:
obj = URIRef(v)
elif metaslot_range == "uriorcurie":
obj = URIRef(this_sv.expand_curie(v))
elif (iri := self._metadata_iri(metaslot_uri, v)) is not None:
# e.g. license and conforms_to: strings in the metamodel, resources by their property
obj = iri
elif metaslot_range in self._LANGUAGE_TAGGABLE_RANGES and lang:
obj = Literal(v, lang=lang)
else:
Expand Down Expand Up @@ -435,12 +482,31 @@ def add_metadata(self, e: Definition | PermissibleValue, uri: URIRef) -> None:
if k_uri == k:
k_uri = None
if k_uri:
if isinstance(v.value, str):
if (iri := self._metadata_iri(URIRef(k_uri), v.value)) is not None:
obj = iri
elif isinstance(v.value, str):
obj = self._literal(v.value, e)
else:
obj = Literal(v.value)
self.graph.add((uri, URIRef(k_uri), obj))

def _metadata_iri(self, prop: URIRef, value: Any) -> URIRef | None:
"""The IRI *value* denotes, if *prop* takes resources and *value* is an IRI or CURIE.

Only the properties in :data:`IRI_VALUED_METADATA_PROPERTIES` take resources by their
specification. A CURIE is expanded when its prefix is declared; any other value that
is a syntactically valid absolute IRI is used as it is. Returns ``None`` otherwise, and
the value is emitted as a literal, as for every other property.
"""
if prop not in IRI_VALUED_METADATA_PROPERTIES or not isinstance(value, str):
return None
if not _ABSOLUTE_IRI.match(value):
return None
prefix = value.split(":", 1)[0]
if prefix in self.schemaview.namespaces():
return URIRef(self.schemaview.expand_curie(value))
return URIRef(value)

def add_class(self, cls: ClassDefinition) -> None:
"""
Each ClassDefinition is represented as an OWL class.
Expand Down
4 changes: 2 additions & 2 deletions tests/linkml/test_base/__snapshots__/meta.owl
Original file line number Diff line number Diff line change
Expand Up @@ -1502,7 +1502,7 @@ linkml:imported_from a owl:ObjectProperty,
linkml:in_language a owl:ObjectProperty,
linkml:SlotDefinition ;
rdfs:label "in_language" ;
dcterms:conformsTo "https://www.rfc-editor.org/rfc/bcp/bcp47.txt" ;
dcterms:conformsTo <https://www.rfc-editor.org/rfc/bcp/bcp47.txt> ;
rdfs:range linkml:String ;
skos:definition "the primary language used in the sources" ;
skos:inScheme linkml:meta ;
Expand Down Expand Up @@ -4785,7 +4785,7 @@ linkml:ClassDefinition a owl:Class,

linkml:meta a owl:Ontology ;
rdfs:label "meta" ;
dcterms:license "https://creativecommons.org/publicdomain/zero/1.0/" ;
dcterms:license <https://creativecommons.org/publicdomain/zero/1.0/> ;
dcterms:title "LinkML Schema Metamodel" ;
skos:definition """The metamodel for schemas defined using the Linked Data Modeling Language framework.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ os:SomeSomeInterpretation a owl:Class , biolink:LogicalInterpretationEnum ;
rdfs:label "some_some" ;
rdfs:subClassOf biolink:LogicalInterpretationEnum ;
skos:definition "A modifier on a triple that causes the triple to be interpreted as a some-some statement" .
<https://w3id.org/biolink/biolink-model> dct:license "https://creativecommons.org/publicdomain/zero/1.0/" ;
<https://w3id.org/biolink/biolink-model> dct:license <https://creativecommons.org/publicdomain/zero/1.0/> ;
pav:version "4.2.5" ;
a owl:Ontology ;
rdfs:label "Biolink-Model" ;
Expand Down
65 changes: 65 additions & 0 deletions tests/linkml/test_generators/test_owlgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -1232,3 +1232,68 @@ def test_complement_of_union_of_mixed_none_filters_silently():
# Should succeed and return a BNode (the complement expression).
assert result is not None
assert isinstance(result, BNode)


_IRI_METADATA_SCHEMA = """
id: https://example.org/onto/v2
name: iri_metadata
license: https://www.eclipse.org/legal/epl-2.0/
conforms_to: https://example.org/onto/v2/shapes
see_also:
- https://example.org/docs
prefixes:
linkml: https://w3id.org/linkml/
ex: https://example.org/
owl: http://www.w3.org/2002/07/owl#
prov: http://www.w3.org/ns/prov#
dcterms: http://purl.org/dc/terms/
default_prefix: ex
imports:
- linkml:types
annotations:
owl:versionInfo: v2
owl:versionIRI: https://example.org/onto/v2
owl:priorVersion: ex:onto/v1
prov:wasDerivedFrom: https://example.org/releases/tag/v2.0.0
dcterms:references: https://example.org/other
dcterms:identifier: https://example.org/onto
dcterms:creator: The Example Team
classes:
Thing:
annotations:
dcterms:license: Not an IRI, just text
"""


def test_iri_valued_metadata_is_emitted_as_iris():
"""Annotations and metadata of properties that take resources are IRIs when their value is one."""
g = Graph()
g.parse(data=OwlSchemaGenerator(_IRI_METADATA_SCHEMA, metaclasses=False).serialize(), format="turtle")
onto = URIRef("https://example.org/onto/v2")
DCT = Namespace("http://purl.org/dc/terms/")
PROV = Namespace("http://www.w3.org/ns/prov#")

assert g.value(onto, OWL.versionIRI) == onto
# a CURIE with a declared prefix is expanded
assert g.value(onto, OWL.priorVersion) == URIRef("https://example.org/onto/v1")
assert g.value(onto, PROV.wasDerivedFrom) == URIRef("https://example.org/releases/tag/v2.0.0")
assert g.value(onto, DCT.references) == URIRef("https://example.org/other")
# string-ranged metamodel slots whose property takes resources
assert g.value(onto, DCT.license) == URIRef("https://www.eclipse.org/legal/epl-2.0/")
assert g.value(onto, DCT.conformsTo) == URIRef("https://example.org/onto/v2/shapes")
assert g.value(onto, RDFS.seeAlso) == URIRef("https://example.org/docs")


def test_literal_valued_metadata_stays_literal():
"""Other properties, and values that are not IRIs, keep their literals."""
g = Graph()
g.parse(data=OwlSchemaGenerator(_IRI_METADATA_SCHEMA, metaclasses=False).serialize(), format="turtle")
onto = URIRef("https://example.org/onto/v2")
DCT = Namespace("http://purl.org/dc/terms/")

assert g.value(onto, OWL.versionInfo) == Literal("v2")
# DCMI gives dcterms:identifier the range rdfs:Literal
assert g.value(onto, DCT.identifier) == Literal("https://example.org/onto")
assert g.value(onto, DCT.creator) == Literal("The Example Team")
# a property that takes resources, with a value that is not an IRI
assert g.value(URIRef("https://example.org/Thing"), DCT.license) == Literal("Not an IRI, just text")
Loading