From a64ffe31e56462970533c917c8a9aad7d4d90253 Mon Sep 17 00:00:00 2001 From: "Francesca L. Bleken" <48128015+francescalb@users.noreply.github.com> Date: Mon, 16 Dec 2024 11:34:01 +0100 Subject: [PATCH] Importing rdfs schemas (#814) # Description Partially closes #811. Closes #797 I is not possible to easily access classes of typ rdfs:Class. This is needed if working simultaneously with an rdf-schema (as opposed to an owl ontology). Owlready2 does not search for enties that are not of type owl:XXX. A search for rdfs:Class is now added so that when asking for classes in an ontology also the rdfs:Class'es are returned as Python objects. This is not as readily fixed with rdfs:Property. As a first fix, such properties are now returned as a list of strings (instead of a list of python objcets). Note that the nodes are present in the onotology, but the rdfs:Poerty is not cast as a python object. Owlready2 is designed to support owl ontologies (not rdfs). If we really want to extend the suport for rdfs:Property the best solution is most likely to cast it to an owl:Annotation_property. If so this should be done in a next PR . --- ontopy/ontology.py | 113 +++++++++++++++++++++---- tests/test_import_foaf.py | 36 -------- tests/test_load.py | 29 +++---- tests/test_load_notemmo.py | 78 +++++++++++++++++ tests/testonto/minischema.ttl | 23 +++++ tests/testonto/minischema_owlclass.ttl | 16 ++++ 6 files changed, 223 insertions(+), 72 deletions(-) delete mode 100644 tests/test_import_foaf.py create mode 100755 tests/test_load_notemmo.py create mode 100644 tests/testonto/minischema.ttl create mode 100644 tests/testonto/minischema_owlclass.ttl diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 1eea4a0e6..12dc9ef90 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -27,6 +27,7 @@ from owlready2.entity import ThingClass from owlready2.prop import ObjectPropertyClass, DataPropertyClass from owlready2 import AnnotationPropertyClass +from owlready2.base import rdf_type from ontopy.factpluspluswrapper.sync_factpp import sync_reasoner_factpp from ontopy.utils import ( # pylint: disable=cyclic-import @@ -51,7 +52,7 @@ ) if TYPE_CHECKING: - from typing import Iterator, List, Sequence + from typing import Iterator, List, Sequence, Generator # Default annotations to look up @@ -1135,19 +1136,50 @@ def rec_imported(onto, imported): def get_entities( # pylint: disable=too-many-arguments self, *, - imported=True, - classes=True, - individuals=True, - object_properties=True, - data_properties=True, - annotation_properties=True, - ): - """Return a generator over (optionally) all classes, individuals, - object_properties, data_properties and annotation_properties. + imported: bool = True, + classes: bool = True, + individuals: bool = True, + object_properties: bool = True, + data_properties: bool = True, + annotation_properties: bool = True, + properties: bool = True, + ) -> "Generator[Union[str, object], None, None]": + """ + This method returns a generator over entities in the ontology, + including the following categories: + - Classes (`owl:Class` or `rdfs:Class`) + - Individuals + - Object properties (`owl:ObjectProperty`) + - Data properties (`owl:DataProperty`) + - Annotation properties (`owl:AnnotationProperty`) + - Properties (`rdfs:Property`) + + Notes: + - If `properties` is `True`, `rdfs:Property` entities will be returned + as IRIs (strings) rather than Python objects. + - When `imported` is `True`, entities from imported ontologies will + also be included. + + Arguments: + imported (bool): Whether to include entities from imported + ontologies. Defaults to `True`. + classes (bool): Whether to include classes. Defaults to `True`. + individuals (bool): Whether to include individuals. + Defaults to `True`. + object_properties (bool): Whether to include object properties. + Defaults to `True`. + data_properties (bool): Whether to include data properties. + Defaults to `True`. + annotation_properties (bool): Whether to include annotation + properties. Defaults to `True`. + properties (bool): Whether to include `rdfs:Property` entities. + Defaults to `True`. + + Yields: + Entities matching the specified filters. - If `imported` is `True`, entities in imported ontologies will also - be included. """ + generator = [] if classes: generator.append(self.classes(imported)) @@ -1159,6 +1191,8 @@ def get_entities( # pylint: disable=too-many-arguments generator.append(self.data_properties(imported)) if annotation_properties: generator.append(self.annotation_properties(imported)) + if properties: + generator.append(self.properties(imported)) yield from itertools.chain(*generator) def classes(self, imported=False): @@ -1175,14 +1209,14 @@ def _entities( ): # pylint: disable=too-many-branches """Returns an generator over all entities of the desired type. This is a helper function for `classes()`, `individuals()`, - `object_properties()`, `data_properties()` and - `annotation_properties()`. + `object_properties()`, `data_properties()`, + `annotation_properties()` and `properties`. Arguments: entity_type: The type of entity desired given as a string. Can be any of `classes`, `individuals`, - `object_properties`, `data_properties` and - `annotation_properties`. + `object_properties`, `data_properties`, + `annotation_properties` or `properties`. imported: if `True`, entities in imported ontologies are also returned. """ @@ -1207,9 +1241,18 @@ def _entities( elif entity_type == "annotation_properties": for prop in list(onto.annotation_properties()): generator.append(prop) + elif entity_type == "properties": + generator.append(list(onto.properties())) else: if entity_type == "classes": - generator = super().classes() + generator = list(super().classes()) + # Add new triples of type rdfs:Class + rdf_schema_class = self._abbreviate( + "http://www.w3.org/2000/01/rdf-schema#Class" + ) + for s in self._get_obj_triples_po_s(rdf_type, rdf_schema_class): + if not s < 0: + generator.append(self.world._get_by_storid(s)) elif entity_type == "individuals": generator = super().individuals() elif entity_type == "object_properties": @@ -1218,6 +1261,8 @@ def _entities( generator = super().data_properties() elif entity_type == "annotation_properties": generator = super().annotation_properties() + elif entity_type == "properties": + generator = self.properties() yield from generator @@ -1258,6 +1303,40 @@ def annotation_properties(self, imported=False): """ return self._entities("annotation_properties", imported=imported) + def properties(self, imported=False): + """Returns an generator over all properties. + It searches for owl:object_properties, owl:data_properties, + owl:annotation_properties and rdf:Properties + + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. + """ + generator = [] + for prop in list( + self._entities("object_properties", imported=imported) + ): + generator.append(prop) + + for prop in list( + self._entities("annotation_properties", imported=imported) + ): + generator.append(prop) + + for prop in list(self._entities("data_properties", imported=imported)): + generator.append(prop) + + rdf_property = self._abbreviate( + "http://www.w3.org/1999/02/22-rdf-syntax-ns#Property" + ) + for s in self._get_obj_triples_po_s(rdf_type, rdf_property): + if not s < 0: + # print(s, self._unabbreviate(s)) + generator.append(self._unabbreviate(s)) + # generator.append(self[self._unabbreviate(s)]) + # generator.append(self.world._get_by_storid(s)) + yield from generator + def get_root_classes(self, imported=False): """Returns a list or root classes.""" return [ diff --git a/tests/test_import_foaf.py b/tests/test_import_foaf.py deleted file mode 100644 index db9462db0..000000000 --- a/tests/test_import_foaf.py +++ /dev/null @@ -1,36 +0,0 @@ -from typing import TYPE_CHECKING - -import pytest - -if TYPE_CHECKING: - from ontopy.ontology import Ontology - - -@pytest.mark.skip("FOAF is currently unavailable.") -def test_import_foaf(emmo: "Ontology") -> None: - """Test importing foaf - - foaf is the Friend-of-a-Friend ontology. - - This test serves more like an example. - TODO: Move to `examples/` - """ - from ontopy import get_ontology - - skos = get_ontology("http://www.w3.org/2004/02/skos/core#").load() - foaf = get_ontology("http://xmlns.com/foaf/0.1/") - - # Needed since foaf refer to skos without importing it - foaf.imported_ontologies.append(skos) - - # Turn off label lookup. Needed because foaf uses labels that are not - # valid Python identifiers - foaf._special_labels = () - - # Now we can load foaf - foaf.load() - - with emmo: - - class Person(emmo.Interpreter): - equivalent_to = [foaf.Person] diff --git a/tests/test_load.py b/tests/test_load.py index 02937e0cc..2a589c74c 100755 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -3,13 +3,18 @@ if TYPE_CHECKING: from pathlib import Path +from pathlib import Path -def test_load(repo_dir: "Path", testonto: "Ontology") -> None: + +def test_load() -> None: # if True: - # from pathlib import Path - # from ontopy import get_ontology - # repo_dir = Path(__file__).resolve().parent.parent - # testonto = get_ontology(str(repo_dir / "tests" / "testonto" / "testonto.ttl")).load() + from pathlib import Path + from ontopy import get_ontology + + repo_dir = Path(__file__).resolve().parent.parent + testonto = get_ontology( + str(repo_dir / "tests" / "testonto" / "testonto.ttl") + ).load() import pytest @@ -52,17 +57,3 @@ def test_load(repo_dir: "Path", testonto: "Ontology") -> None: "datamodel-ontology/master/datamodel.ttl" ).load() assert onto.DataModel - - -def test_load_rdfs() -> None: - """Test to load non-emmo based ontologies rdf and rdfs""" - from ontopy import get_ontology - - rdf_onto = get_ontology( - "https://www.w3.org/1999/02/22-rdf-syntax-ns.ttl" - ).load(emmo_based=False) - rdfs_onto = get_ontology("https://www.w3.org/2000/01/rdf-schema.ttl").load( - emmo_based=False - ) - rdfs_onto.Class # Needed to initialize rdfs_onto - assert rdf_onto.HTML.is_a[0].iri == rdfs_onto.Datatype.iri diff --git a/tests/test_load_notemmo.py b/tests/test_load_notemmo.py new file mode 100755 index 000000000..8f2b943c3 --- /dev/null +++ b/tests/test_load_notemmo.py @@ -0,0 +1,78 @@ +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from pathlib import Path + +from pathlib import Path + + +def test_load_rdfs() -> None: + """Test to load non-emmo based ontologies rdf and rdfs""" + from ontopy import get_ontology + + rdf_onto = get_ontology( + "https://www.w3.org/1999/02/22-rdf-syntax-ns.ttl" + ).load(emmo_based=False) + rdfs_onto = get_ontology("https://www.w3.org/2000/01/rdf-schema.ttl").load( + emmo_based=False + ) + rdfs_onto.Class # Needed to initialize rdfs_onto + assert rdf_onto.HTML.is_a[0].iri == rdfs_onto.Datatype.iri + + +def test_load_schema() -> None: + """Test to load non-emmo based ontologies rdf and rdfs""" + from ontopy import get_ontology + + repo_dir = Path(__file__).resolve().parent + onto = get_ontology(repo_dir / "testonto" / "minischema.ttl").load( + emmo_based=False + ) + assert list(onto.classes()) == [onto.AMRadioChannel] + onto_owlclass = get_ontology( + repo_dir / "testonto" / "minischema_owlclass.ttl" + ).load(emmo_based=False) + assert list(onto_owlclass.classes()) == [onto_owlclass.AMRadioChannel] + + assert list(onto.properties()) == ["https://schema.org/abridged"] + + # Should be: + # assert list(onto.properties()) == [onto.abridged] + + +# @pytest.mark.skip("FOAF is currently unavailable.") +# def test_import_foaf(emmo: "Ontology") -> None: +if True: + """Test importing foaf + + foaf is the Friend-of-a-Friend ontology. + + This test serves more like an example. + TODO: Move to `examples/` + """ + from ontopy import get_ontology + + emmo = get_ontology("emmo").load() + # skos = get_ontology("https://www.w3.org/2009/08/skos-reference/skos.html#SKOS-RDF").load() + foaf = get_ontology("http://xmlns.com/foaf/spec/index.rdf").load() + + # Needed since foaf refer to skos without importing it + # foaf.imported_ontologies.append(skos) + + # Turn off label lookup. Needed because foaf uses labels that are not + # valid Python identifiers + # foaf._special_labels = () + + # Now we can load foaf + # foaf.load() + + with emmo: + + class Person(emmo.Interpreter): + equivalent_to = [foaf.Person] + + +# def test_load_qudt: +# if True: +# units = get_ontology("http://qudt.org/2.1/vocab/unit").load() +# rdflib comppains. Apparently it cannot serialize it once it is loaded. diff --git a/tests/testonto/minischema.ttl b/tests/testonto/minischema.ttl new file mode 100644 index 000000000..327d1ac8b --- /dev/null +++ b/tests/testonto/minischema.ttl @@ -0,0 +1,23 @@ +@prefix dcat: . +@prefix dcmitype: . +@prefix dcterms: . +@prefix foaf: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix schema: . +@prefix skos: . +@prefix void: . + +schema:AMRadioChannel a rdfs:Class ; + rdfs:label "AMRadioChannel" ; + rdfs:comment "A radio channel that uses AM." ; + rdfs:subClassOf schema:RadioChannel ; + schema:source . + +schema:abridged a rdf:Property ; + rdfs:label "abridged" ; + rdfs:comment "Indicates whether the book is an abridged edition." ; + schema:domainIncludes schema:Book ; + schema:isPartOf ; + schema:rangeIncludes schema:Boolean . diff --git a/tests/testonto/minischema_owlclass.ttl b/tests/testonto/minischema_owlclass.ttl new file mode 100644 index 000000000..4b6a991a6 --- /dev/null +++ b/tests/testonto/minischema_owlclass.ttl @@ -0,0 +1,16 @@ +@prefix dcat: . +@prefix dcmitype: . +@prefix dcterms: . +@prefix foaf: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix schema: . +@prefix skos: . +@prefix void: . + +schema:AMRadioChannel a owl:Class ; + rdfs:label "AMRadioChannel" ; + rdfs:comment "A radio channel that uses AM." ; + rdfs:subClassOf schema:RadioChannel ; + schema:source .