From f2db0e9438a225d8eb52005682567421f82bd7b6 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 4 Apr 2024 08:07:25 +0200 Subject: [PATCH 01/24] Initiated ontodoc2 --- ontopy/ontodoc2.py | 69 ++++++++++++++++++++++++++++++++++++++++++ tests/test_ontodoc2.py | 18 +++++++++++ 2 files changed, 87 insertions(+) create mode 100644 ontopy/ontodoc2.py create mode 100644 tests/test_ontodoc2.py diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py new file mode 100644 index 000000000..f4e7009a8 --- /dev/null +++ b/ontopy/ontodoc2.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +""" +A module for documenting ontologies. +""" +# pylint: disable=fixme,too-many-lines,no-member +import os +import re +import time +import warnings +import shlex +import shutil +import subprocess # nosec +from textwrap import dedent +from tempfile import NamedTemporaryFile, TemporaryDirectory +from typing import TYPE_CHECKING + +import yaml +import owlready2 + +from ontopy.utils import asstring, camelsplit, get_label, get_format + +if TYPE_CHECKING: + from pathlib import Path + from typing import Iterable, Union + + from ontopy import Ontology + + Cls: Type[owlready2.Thing] + Property: Type[owlready2.Property] + Individual: owlready2.Thing + + +class OntoDoc: + """Class for documentating ontologies. + + Arguments: + ontologies: Ontologies to include in the generated documentation. + All entities in these ontologies will be included. + entities: Explicit listing of entities (classes, properties, + individuals) to document. + imported: Whether to recursively include imported ontologies. + + + """ + + def __init__( + self, + ontologies: "Iterable[Ontology]" = None, + entities: "Iterable[Union[Cls, Property, Individual]]" = None, + imported: bool = False, + ) -> None: + if ontologies: + for onto in ontologies: + self.add_ontology(onto, imported=imported) + + if entities: + for entity in entities: + self.add_entity(entity) + + self.entities = set() + + def add_entity(self, entity: "Union[Cls, Property, Individual]") -> None: + """Add `entity` (class, property, individual) to list of entities to + document.""" + self.entities.add(entity) + + def add_ontology(self, onto: "Ontology", imported: bool = False) -> None: + """Add ontology `onto` to documentation.""" + self.entities.update(onto.get_entities(imported=imported)) diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc2.py new file mode 100644 index 000000000..c4a65fbe6 --- /dev/null +++ b/tests/test_ontodoc2.py @@ -0,0 +1,18 @@ +# if True: +def test_ontodoc(): + """Test ontodoc.""" + from pathlib import Path + + from ontopy.ontodoc2 import OntoDoc + from ontopy import get_ontology + import owlready2 + import owlready2.reasoning + + owlready2.reasoning.JAVA_MEMORY = 28000 + + # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() + emmo = get_ontology("../EMMO/emmo.ttl").load() + emmo.sync_reasoner(include_imported=True) + + ontologies = emmo.get_imported_ontologies(True) + # owlready2.sync_reasoner(ontologies) From 80a020d38ab5936b55c3ee4b325bd970fe47ba7b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 4 Apr 2024 08:19:49 +0200 Subject: [PATCH 02/24] Updated ontodoc2 --- ontopy/ontodoc2.py | 1 - tests/test_ontodoc2.py | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py index f4e7009a8..e911d5e7f 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc2.py @@ -40,7 +40,6 @@ class OntoDoc: individuals) to document. imported: Whether to recursively include imported ontologies. - """ def __init__( diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc2.py index c4a65fbe6..0801191f9 100644 --- a/tests/test_ontodoc2.py +++ b/tests/test_ontodoc2.py @@ -1,3 +1,6 @@ +"""Test ontodoc""" + + # if True: def test_ontodoc(): """Test ontodoc.""" @@ -12,7 +15,7 @@ def test_ontodoc(): # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() emmo = get_ontology("../EMMO/emmo.ttl").load() - emmo.sync_reasoner(include_imported=True) + # emmo.sync_reasoner(include_imported=True) - ontologies = emmo.get_imported_ontologies(True) + # ontologies = emmo.get_imported_ontologies(True) # owlready2.sync_reasoner(ontologies) From bfcc9a62a8dd8c7f3b07d36beff41def104af9c4 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 4 Apr 2024 08:47:13 +0200 Subject: [PATCH 03/24] work in progress... --- ontopy/ontodoc2.py | 31 ++++++++++++++++++++++++++++--- tests/test_ontodoc2.py | 5 ++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py index e911d5e7f..1767fd4d8 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc2.py @@ -56,13 +56,38 @@ def __init__( for entity in entities: self.add_entity(entity) - self.entities = set() + self.classes = set() + self.object_properties = set() + self.data_properties = set() + self.annotation_properties = set() + self.datatypes = set() + self.individuals = set() def add_entity(self, entity: "Union[Cls, Property, Individual]") -> None: """Add `entity` (class, property, individual) to list of entities to document.""" - self.entities.add(entity) + if isinstance(entity, owlready2.ThingClass): + self.classes.add(entity) + elif isinstance(entity, owlready2.ObjectPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.DataPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.AnnotationPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.Thing): + if ( + hasattr(entity.__class__, "iri") + and entity.__class__.iri + == "http://www.w3.org/2000/01/rdf-schema#Datatype" + ): + self.datatypes.add(entity) + else: + self.individuals.add(entity) def add_ontology(self, onto: "Ontology", imported: bool = False) -> None: """Add ontology `onto` to documentation.""" - self.entities.update(onto.get_entities(imported=imported)) + for entity in onto.get_entities(imported=imported): + self.add_entity(entity) + + def write_header(self): + """ """ diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc2.py index 0801191f9..cd3c8f5e7 100644 --- a/tests/test_ontodoc2.py +++ b/tests/test_ontodoc2.py @@ -1,8 +1,7 @@ """Test ontodoc""" - -# if True: -def test_ontodoc(): +if True: + # def test_ontodoc(): """Test ontodoc.""" from pathlib import Path From c6db97952d5415c85736c16a5b375c0eac4cf43c Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 4 Apr 2024 17:54:37 +0200 Subject: [PATCH 04/24] Separated documentation into several classes. --- ontopy/ontodoc2.py | 92 ++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 3 deletions(-) diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py index 1767fd4d8..37a974b28 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc2.py @@ -14,6 +14,8 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory from typing import TYPE_CHECKING +from rdflib import DCTERMS + import yaml import owlready2 @@ -27,11 +29,95 @@ Cls: Type[owlready2.Thing] Property: Type[owlready2.Property] - Individual: owlready2.Thing + Individual: owlready2.Thing # also datatype + Entity: Union[Cls, Property, Individual] + + +class ModuleDocumentation: + """Class for documentating a module in an ontology. + + Arguments: + ontology: Ontology to include in the generated documentation. + All entities in this ontology will be included. + entities: Explicit listing of entities (classes, properties, + individuals, datatypes) to document. Normally not needed. + title: Header title. Be default it is inferred from title of + """ + + def __init__( + self, + ontology: "Ontology" = None, + entities: "Iterable[Entity]" = None, + ) -> None: + self.ontology = ontology + + if title: + self.title = title + elif ontology: + titleid = ontology._abbreviate(DCTERMS.title) + _, _, title = ontology.get_triples(ontology.storid, titleid)[0] + if title.endswith("@en"): # fixme: should work for all languages + title = title[:-3].strip("'\"") + self.title = title + + self.classes = set() + self.object_properties = set() + self.data_properties = set() + self.annotation_properties = set() + self.datatypes = set() + self.individuals = set() + + if ontology: + self.add_ontology(ontology) + + if entities: + for entity in entities: + self.add_entity(entity) + + def add_entity(self, entity: "Entity") -> None: + """Add `entity` (class, property, individual, datatype) to list of + entities to document. + """ + if isinstance(entity, owlready2.ThingClass): + self.classes.add(entity) + elif isinstance(entity, owlready2.ObjectPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.DataPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.AnnotationPropertyClass): + self.object_properties.add(entity) + elif isinstance(entity, owlready2.Thing): + if ( + hasattr(entity.__class__, "iri") + and entity.__class__.iri + == "http://www.w3.org/2000/01/rdf-schema#Datatype" + ): + self.datatypes.add(entity) + else: + self.individuals.add(entity) + + def add_ontology( + self, ontology: "Ontology", imported: bool = False + ) -> None: + """Add ontology to documentation.""" + for entity in onto.get_entities(imported=imported): + self.add_entity(entity) + + def write_header(self) -> str: + """Return a the reStructuredText header as a string.""" + return f""" +========== +References +========== + +{self.title} +================ + +""" -class OntoDoc: - """Class for documentating ontologies. +class OntologyDocumentation: + """Documentation for an ontology with a common namespace. Arguments: ontologies: Ontologies to include in the generated documentation. From 06ed3f5e84f63a884623ab54cff8af4a02322a87 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 5 Apr 2024 07:44:10 +0200 Subject: [PATCH 05/24] Updated ontodoc --- ontopy/ontodoc2.py | 153 ++++++++++++++++++++++++----------------- ontopy/utils.py | 8 +++ tests/test_ontodoc2.py | 10 ++- 3 files changed, 101 insertions(+), 70 deletions(-) diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py index 37a974b28..14e3a1225 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc2.py @@ -14,18 +14,17 @@ from tempfile import NamedTemporaryFile, TemporaryDirectory from typing import TYPE_CHECKING -from rdflib import DCTERMS - -import yaml -import owlready2 +import rdflib +from rdflib import DCTERMS, URIRef +from ontopy.ontology import Ontology from ontopy.utils import asstring, camelsplit, get_label, get_format +import owlready2 + if TYPE_CHECKING: from pathlib import Path - from typing import Iterable, Union - - from ontopy import Ontology + from typing import Iterable, Optional, Union Cls: Type[owlready2.Thing] Property: Type[owlready2.Property] @@ -46,20 +45,15 @@ class ModuleDocumentation: def __init__( self, - ontology: "Ontology" = None, - entities: "Iterable[Entity]" = None, + ontology: "Optional[Ontology]" = None, + entities: "Optional[Iterable[Entity]]" = None, + title: "Optional[str]" = None, ) -> None: self.ontology = ontology - - if title: - self.title = title - elif ontology: - titleid = ontology._abbreviate(DCTERMS.title) - _, _, title = ontology.get_triples(ontology.storid, titleid)[0] - if title.endswith("@en"): # fixme: should work for all languages - title = title[:-3].strip("'\"") - self.title = title - + self.title = title + self.graph = ( + ontology.world.as_rdflib_graph() if ontology else rdflib.Graph() + ) self.classes = set() self.object_properties = set() self.data_properties = set() @@ -100,21 +94,89 @@ def add_ontology( self, ontology: "Ontology", imported: bool = False ) -> None: """Add ontology to documentation.""" - for entity in onto.get_entities(imported=imported): + for entity in ontology.get_entities(imported=imported): self.add_entity(entity) - def write_header(self) -> str: + def get_header(self) -> str: """Return a the reStructuredText header as a string.""" + if self.title: + title = self.title + elif self.ontology: + iri = self.ontology.base_iri.rstrip("#/") + title = self.graph.value(URIRef(iri), DCTERMS.title) + else: + title = "" + return f""" -========== -References -========== -{self.title} -================ +{title} +{'='*len(title)} """ + def get_refdoc( + self, + subsections="classes,object_properties,data_properties,annotation_properties,individuals,datatypes", + ): + """Return reference documentation of all module entities. + + `subsections` is a comma-separated list of subsections to + return documentation for. + """ + maps = { + "classes": self.classes, + "object_properties": self.object_properties, + "data_properties": self.data_properties, + "annotation_properties": self.annotation_properties, + "individuals": self.individuals, + "datatypes": self.datatypes, + } + lines = [] + + for subsection in subsections.split(","): + if maps[subsection]: + lines.extend( + [ + subsection.replace("_", " ").title(), + "-" * len(subsection), + "", + ] + ) + for entity in sorted(maps[subsection], key=lambda e: get_label(e)): + label = str(get_label(entity)) + lines.extend( + [ + ".. raw:: html", + "", + f'
', + "", + label, + "-" * len(label), + "", + f"* {entity.iri}", + "", + ".. raw:: html", + "", + ' ', + ] + ) + for key, value in entity.get_annotations(): + lines.extend( + [ + " ", + f' ', + f' ', + " ", + ] + ) + lines.extend( + [ + "
{key.title()}{value}
", + "", + ] + ) + return "\n".join(lines) + class OntologyDocumentation: """Documentation for an ontology with a common namespace. @@ -130,8 +192,7 @@ class OntologyDocumentation: def __init__( self, - ontologies: "Iterable[Ontology]" = None, - entities: "Iterable[Union[Cls, Property, Individual]]" = None, + ontologies: "Iterable[Ontology]" = (), imported: bool = False, ) -> None: if ontologies: @@ -141,39 +202,3 @@ def __init__( if entities: for entity in entities: self.add_entity(entity) - - self.classes = set() - self.object_properties = set() - self.data_properties = set() - self.annotation_properties = set() - self.datatypes = set() - self.individuals = set() - - def add_entity(self, entity: "Union[Cls, Property, Individual]") -> None: - """Add `entity` (class, property, individual) to list of entities to - document.""" - if isinstance(entity, owlready2.ThingClass): - self.classes.add(entity) - elif isinstance(entity, owlready2.ObjectPropertyClass): - self.object_properties.add(entity) - elif isinstance(entity, owlready2.DataPropertyClass): - self.object_properties.add(entity) - elif isinstance(entity, owlready2.AnnotationPropertyClass): - self.object_properties.add(entity) - elif isinstance(entity, owlready2.Thing): - if ( - hasattr(entity.__class__, "iri") - and entity.__class__.iri - == "http://www.w3.org/2000/01/rdf-schema#Datatype" - ): - self.datatypes.add(entity) - else: - self.individuals.add(entity) - - def add_ontology(self, onto: "Ontology", imported: bool = False) -> None: - """Add ontology `onto` to documentation.""" - for entity in onto.get_entities(imported=imported): - self.add_entity(entity) - - def write_header(self): - """ """ diff --git a/ontopy/utils.py b/ontopy/utils.py index 010e5e751..9f3c45100 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -96,6 +96,14 @@ def isinteractive(): def get_label(entity): """Returns the label of an entity.""" + onto = entity.namespace.ontology + for la in onto.label_annotations: + try: + label = entity[la] + if label: + return lable + except (NoSuchLabelError, AttributeError): + continue if hasattr(entity, "prefLabel") and entity.prefLabel: return entity.prefLabel.first() if hasattr(entity, "label") and entity.label: diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc2.py index cd3c8f5e7..f2e13f623 100644 --- a/tests/test_ontodoc2.py +++ b/tests/test_ontodoc2.py @@ -5,16 +5,14 @@ """Test ontodoc.""" from pathlib import Path - from ontopy.ontodoc2 import OntoDoc from ontopy import get_ontology + from ontopy.ontodoc2 import ModuleDocumentation import owlready2 - import owlready2.reasoning - - owlready2.reasoning.JAVA_MEMORY = 28000 # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() emmo = get_ontology("../EMMO/emmo.ttl").load() # emmo.sync_reasoner(include_imported=True) - # ontologies = emmo.get_imported_ontologies(True) - # owlready2.sync_reasoner(ontologies) + md = ModuleDocumentation(emmo) + print(md.get_header()) + print(md.get_refdoc()) From 30f21b995e7927068c8bf7fb04ad941d013b9a2a Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 6 Apr 2024 10:49:54 +0200 Subject: [PATCH 06/24] Further updated ontodoc2 --- docs/api_reference/ontopy/ontodoc2.md | 3 + ontopy/ontodoc2.py | 167 ++++++++++++++++++-------- ontopy/utils.py | 5 +- tests/test_ontodoc2.py | 7 +- 4 files changed, 129 insertions(+), 53 deletions(-) create mode 100644 docs/api_reference/ontopy/ontodoc2.md diff --git a/docs/api_reference/ontopy/ontodoc2.md b/docs/api_reference/ontopy/ontodoc2.md new file mode 100644 index 000000000..6e03def70 --- /dev/null +++ b/docs/api_reference/ontopy/ontodoc2.md @@ -0,0 +1,3 @@ +# ontodoc2 + +::: ontopy.ontodoc2 diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc2.py index 14e3a1225..952b93417 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc2.py @@ -3,15 +3,7 @@ A module for documenting ontologies. """ # pylint: disable=fixme,too-many-lines,no-member -import os -import re -import time -import warnings -import shlex -import shutil -import subprocess # nosec -from textwrap import dedent -from tempfile import NamedTemporaryFile, TemporaryDirectory +from pathlib import Path from typing import TYPE_CHECKING import rdflib @@ -58,8 +50,8 @@ def __init__( self.object_properties = set() self.data_properties = set() self.annotation_properties = set() - self.datatypes = set() self.individuals = set() + self.datatypes = set() if ontology: self.add_ontology(ontology) @@ -68,6 +60,18 @@ def __init__( for entity in entities: self.add_entity(entity) + def nonempty(self) -> bool: + """Returns whether the module has any classes, properties, individuals + or datatypes.""" + return ( + self.classes + or self.object_properties + or self.data_properties + or self.annotation_properties + or self.individuals + or self.datatypes + ) + def add_entity(self, entity: "Entity") -> None: """Add `entity` (class, property, individual, datatype) to list of entities to document. @@ -104,24 +108,32 @@ def get_header(self) -> str: elif self.ontology: iri = self.ontology.base_iri.rstrip("#/") title = self.graph.value(URIRef(iri), DCTERMS.title) - else: - title = "" + if not title: + title = self.ontology.name + heading = f"Module: {title}" return f""" -{title} -{'='*len(title)} +{heading.title()} +{'='*len(heading)} """ def get_refdoc( self, - subsections="classes,object_properties,data_properties,annotation_properties,individuals,datatypes", - ): + subsections: str = "classes,object_properties,data_properties,annotation_properties,individuals,datatypes", + header: bool = True, + ) -> str: """Return reference documentation of all module entities. - `subsections` is a comma-separated list of subsections to - return documentation for. + Arguments: + subsections: Comma-separated list of subsections to include in + the returned documentation. + header: Whether to also include the header in the returned + documentation. + + Returns: + String with reference documentation. """ maps = { "classes": self.classes, @@ -133,10 +145,14 @@ def get_refdoc( } lines = [] + if header: + lines.append(self.get_header()) + for subsection in subsections.split(","): if maps[subsection]: lines.extend( [ + "-" * len(subsection), subsection.replace("_", " ").title(), "-" * len(subsection), "", @@ -150,31 +166,28 @@ def get_refdoc( "", f'
', "", - label, + f"{label}", "-" * len(label), "", f"* {entity.iri}", "", ".. raw:: html", "", - ' ', - ] - ) - for key, value in entity.get_annotations(): - lines.extend( - [ - " ", - f' ', - f' ', - " ", - ] - ) - lines.extend( - [ - "
{key.title()}{value}
", - "", ] ) + if hasattr(entity, "get_annotations"): + lines.append(' ') + for key, value in entity.get_annotations().items(): + lines.extend( + [ + " ", + f' ', + f' ', + " ", + ] + ) + lines.extend(["
{key.title()}{value}
", ""]) + return "\n".join(lines) @@ -184,21 +197,81 @@ class OntologyDocumentation: Arguments: ontologies: Ontologies to include in the generated documentation. All entities in these ontologies will be included. - entities: Explicit listing of entities (classes, properties, - individuals) to document. - imported: Whether to recursively include imported ontologies. - + imported: Whether to include imported ontologies. + recursive: Whether to recursively import all imported ontologies. + Implies `recursive=True`. + iri_match: A regular expression that the `base_iri` of imported + ontologies should match in order to be included. """ def __init__( self, - ontologies: "Iterable[Ontology]" = (), - imported: bool = False, + ontologies: "Iterable[Ontology]", + imported: bool = True, + recursive: bool = False, + iri_match: "Optional[str]" = None, ) -> None: - if ontologies: - for onto in ontologies: - self.add_ontology(onto, imported=imported) + if isinstance(ontologies, (Ontology, str, Path)): + ontologies = [ontologies] - if entities: - for entity in entities: - self.add_entity(entity) + if recursive: + imported = True + + self.module_documentations = [] + + # Explicitly included ontologies + included_ontologies = [] + for onto in ontologies: + if isinstance(onto, (str, Path)): + onto = get_ontology(onto).load() + elif not isinstance(onto, Ontology): + raise TypeError( + "expected ontology as an IRI, Path or Ontology object, " + f"got: {onto}" + ) + if onto not in included_ontologies: + included_ontologies.append(onto) + + # Indirectly included ontologies (imported) + if imported: + for onto in included_ontologies: + ontos = onto.get_imported_ontologies(recursive=recursive) + if iri_match: + ontos = [ + o for o in ontos if re.match(iri_match, o.base_iri) + ] + for o in ontos: + if o not in included_ontologies: + included_ontologies.append(o) + + # Module documentations + for onto in included_ontologies: + self.module_documentations.append(ModuleDocumentation(onto)) + + def get_header(self) -> str: + """Return a the reStructuredText header as a string.""" + return f""" +========== +References +========== +""" + + def get_refdoc(self, header: bool = True) -> str: + """Return reference documentation of all module entities. + + Arguments: + header: Whether to also include the header in the returned + documentation. + + Returns: + String with reference documentation. + """ + moduledocs = [] + if header: + moduledocs.append(self.get_header()) + moduledocs.extend( + md.get_refdoc() + for md in self.module_documentations + if md.nonempty() + ) + return "\n".join(moduledocs) diff --git a/ontopy/utils.py b/ontopy/utils.py index 9f3c45100..eaa4d14d1 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -101,8 +101,9 @@ def get_label(entity): try: label = entity[la] if label: - return lable - except (NoSuchLabelError, AttributeError): + print("*** LABEL:", repr(label)) + return label + except (NoSuchLabelError, AttributeError, TypeError): continue if hasattr(entity, "prefLabel") and entity.prefLabel: return entity.prefLabel.first() diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc2.py index f2e13f623..05439dbba 100644 --- a/tests/test_ontodoc2.py +++ b/tests/test_ontodoc2.py @@ -6,13 +6,12 @@ from pathlib import Path from ontopy import get_ontology - from ontopy.ontodoc2 import ModuleDocumentation + from ontopy.ontodoc2 import OntologyDocumentation import owlready2 # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() emmo = get_ontology("../EMMO/emmo.ttl").load() # emmo.sync_reasoner(include_imported=True) - md = ModuleDocumentation(emmo) - print(md.get_header()) - print(md.get_refdoc()) + od = OntologyDocumentation(emmo) + print(od.get_refdoc()) From 849c62bfcccd2c995305689713b258d899b9d4dc Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 6 Apr 2024 16:48:30 +0200 Subject: [PATCH 07/24] Updated ontodoc tool to also generate rst output --- docs/api_reference/ontopy/ontodoc2.md | 3 - docs/api_reference/ontopy/ontodoc_rst.md | 3 + ontopy/{ontodoc2.py => ontodoc_rst.py} | 142 ++++++++++++++---- ontopy/patch.py | 13 +- ontopy/utils.py | 7 +- .../{test_ontodoc2.py => test_ontodoc_rst.py} | 6 +- tools/ontodoc | 82 ++++++---- 7 files changed, 185 insertions(+), 71 deletions(-) delete mode 100644 docs/api_reference/ontopy/ontodoc2.md create mode 100644 docs/api_reference/ontopy/ontodoc_rst.md rename ontopy/{ontodoc2.py => ontodoc_rst.py} (65%) rename tests/{test_ontodoc2.py => test_ontodoc_rst.py} (69%) diff --git a/docs/api_reference/ontopy/ontodoc2.md b/docs/api_reference/ontopy/ontodoc2.md deleted file mode 100644 index 6e03def70..000000000 --- a/docs/api_reference/ontopy/ontodoc2.md +++ /dev/null @@ -1,3 +0,0 @@ -# ontodoc2 - -::: ontopy.ontodoc2 diff --git a/docs/api_reference/ontopy/ontodoc_rst.md b/docs/api_reference/ontopy/ontodoc_rst.md new file mode 100644 index 000000000..6d261f529 --- /dev/null +++ b/docs/api_reference/ontopy/ontodoc_rst.md @@ -0,0 +1,3 @@ +# ontodoc_rst + +::: ontopy.ontodoc_rst diff --git a/ontopy/ontodoc2.py b/ontopy/ontodoc_rst.py similarity index 65% rename from ontopy/ontodoc2.py rename to ontopy/ontodoc_rst.py index 952b93417..d6104452f 100644 --- a/ontopy/ontodoc2.py +++ b/ontopy/ontodoc_rst.py @@ -3,25 +3,27 @@ A module for documenting ontologies. """ # pylint: disable=fixme,too-many-lines,no-member +import re +import warnings +from html import escape from pathlib import Path from typing import TYPE_CHECKING import rdflib from rdflib import DCTERMS, URIRef -from ontopy.ontology import Ontology -from ontopy.utils import asstring, camelsplit, get_label, get_format +from ontopy.ontology import Ontology, get_ontology +from ontopy.utils import get_label -import owlready2 +import owlready2 # pylint: disable=wrong-import-order if TYPE_CHECKING: - from pathlib import Path - from typing import Iterable, Optional, Union + from typing import Iterable, Optional, Type, Union - Cls: Type[owlready2.Thing] - Property: Type[owlready2.Property] - Individual: owlready2.Thing # also datatype - Entity: Union[Cls, Property, Individual] + Cls = Type[owlready2.Thing] + Property = Type[owlready2.Property] + Individual = owlready2.Thing # also datatype + Entity = Union[Cls, Property, Individual] class ModuleDocumentation: @@ -33,16 +35,22 @@ class ModuleDocumentation: entities: Explicit listing of entities (classes, properties, individuals, datatypes) to document. Normally not needed. title: Header title. Be default it is inferred from title of + iri_regex: A regular expression that the IRI of documented entities + should match. """ + # pylint: disable=too-many-instance-attributes + def __init__( self, ontology: "Optional[Ontology]" = None, entities: "Optional[Iterable[Entity]]" = None, title: "Optional[str]" = None, + iri_regex: "Optional[str]" = None, ) -> None: self.ontology = ontology self.title = title + self.iri_regex = iri_regex self.graph = ( ontology.world.as_rdflib_graph() if ontology else rdflib.Graph() ) @@ -76,6 +84,9 @@ def add_entity(self, entity: "Entity") -> None: """Add `entity` (class, property, individual, datatype) to list of entities to document. """ + if self.iri_regex and not re.match(self.iri_regex, entity.iri): + return + if isinstance(entity, owlready2.ThingClass): self.classes.add(entity) elif isinstance(entity, owlready2.ObjectPropertyClass): @@ -121,20 +132,34 @@ def get_header(self) -> str: def get_refdoc( self, - subsections: str = "classes,object_properties,data_properties,annotation_properties,individuals,datatypes", + subsections: str = "all", header: bool = True, ) -> str: """Return reference documentation of all module entities. Arguments: subsections: Comma-separated list of subsections to include in - the returned documentation. + the returned documentation. Valid subsection names are: + - classes + - object_properties + - data_properties + - annotation_properties + - individuals + - datatypes + If "all", all subsections will be documented. header: Whether to also include the header in the returned documentation. Returns: String with reference documentation. """ + # pylint: disable=too-many-nested-blocks + if subsections == "all": + subsections = ( + "classes,object_properties,data_properties," + "annotation_properties,individuals,datatypes" + ) + maps = { "classes": self.classes, "object_properties": self.object_properties, @@ -148,6 +173,20 @@ def get_refdoc( if header: lines.append(self.get_header()) + def add_keyvalue(key, value): + """Help function for adding a key-value row to table.""" + escaped = escape(str(value)).replace("\n", "
") + lines.extend( + [ + " ", + ' ' + f'' + f"{key.title()}", + f' {escaped}', + " ", + ] + ) + for subsection in subsections.split(","): if maps[subsection]: lines.extend( @@ -158,8 +197,8 @@ def get_refdoc( "", ] ) - for entity in sorted(maps[subsection], key=lambda e: get_label(e)): - label = str(get_label(entity)) + for entity in sorted(maps[subsection], key=get_label): + label = get_label(entity) lines.extend( [ ".. raw:: html", @@ -178,14 +217,11 @@ def get_refdoc( if hasattr(entity, "get_annotations"): lines.append(' ') for key, value in entity.get_annotations().items(): - lines.extend( - [ - " ", - f' ', - f' ', - " ", - ] - ) + if isinstance(value, list): + for val in value: + add_keyvalue(key, val) + else: + add_keyvalue(key, value) lines.extend(["
{key.title()}{value}
", ""]) return "\n".join(lines) @@ -200,8 +236,8 @@ class OntologyDocumentation: imported: Whether to include imported ontologies. recursive: Whether to recursively import all imported ontologies. Implies `recursive=True`. - iri_match: A regular expression that the `base_iri` of imported - ontologies should match in order to be included. + iri_regex: A regular expression that the IRI of documented entities + should match. """ def __init__( @@ -209,7 +245,7 @@ def __init__( ontologies: "Iterable[Ontology]", imported: bool = True, recursive: bool = False, - iri_match: "Optional[str]" = None, + iri_regex: "Optional[str]" = None, ) -> None: if isinstance(ontologies, (Ontology, str, Path)): ontologies = [ontologies] @@ -217,6 +253,7 @@ def __init__( if recursive: imported = True + self.iri_regex = iri_regex self.module_documentations = [] # Explicitly included ontologies @@ -234,23 +271,20 @@ def __init__( # Indirectly included ontologies (imported) if imported: - for onto in included_ontologies: - ontos = onto.get_imported_ontologies(recursive=recursive) - if iri_match: - ontos = [ - o for o in ontos if re.match(iri_match, o.base_iri) - ] - for o in ontos: + for onto in included_ontologies[:]: + for o in onto.get_imported_ontologies(recursive=recursive): if o not in included_ontologies: included_ontologies.append(o) # Module documentations for onto in included_ontologies: - self.module_documentations.append(ModuleDocumentation(onto)) + self.module_documentations.append( + ModuleDocumentation(onto, iri_regex=iri_regex) + ) def get_header(self) -> str: """Return a the reStructuredText header as a string.""" - return f""" + return """ ========== References ========== @@ -275,3 +309,45 @@ def get_refdoc(self, header: bool = True) -> str: if md.nonempty() ) return "\n".join(moduledocs) + + def top_ontology(self) -> Ontology: + """Return the top-level ontology.""" + return self.module_documentations[0].ontology + + def write_refdoc(self, docfile=None): + """Write reference documentation to disk. + + Arguments: + docfile: Name of file to write to. Defaults to the name of + the top ontology with extension `.rst`. + """ + if not docfile: + docfile = self.top_ontology().name + ".rst" + Path(docfile).write_text(self.get_refdoc(), encoding="utf8") + + def write_index_template( + self, indexfile="index.rst", docfile=None, overwrite=False + ): + """Write a basic template index.rst file to disk. + + Arguments: + indexfile: Name of index file to write. + docfile: Name of generated documentation file. If not given, + the name of the top ontology will be used. + overwrite: Whether to overwrite an existing file. + """ + docname = Path(docfile).stem if docfile else self.top_ontology().name + content = f""" +.. toctree:: + :includehidden: + :hidden: + + Reference Index <{docname}> + +""" + outpath = Path(indexfile) + if not overwrite and outpath.exists(): + warnings.warn(f"index.rst file already exists: {outpath}") + return + + outpath.write_text(content, encoding="utf8") diff --git a/ontopy/patch.py b/ontopy/patch.py index 07d0411d7..37519a3f1 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -157,10 +157,19 @@ def get_annotations( """ onto = self.namespace.ontology + def extend(key, values): + """Extend annotations with a sequence of values.""" + if key in annotations: + annotations[key].extend(values) + else: + annotations[key] = values + annotations = { - str(get_preferred_label(_)): _._get_values_for_class(self) - for _ in onto.annotation_properties(imported=imported) + str(get_preferred_label(a)): a._get_values_for_class(self) + for a in onto.annotation_properties(imported=imported) } + extend("comment", self.comment) + extend("label", self.label) if all: return annotations return {key: value for key, value in annotations.items() if value} diff --git a/ontopy/utils.py b/ontopy/utils.py index eaa4d14d1..54ba6e301 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -96,13 +96,13 @@ def isinteractive(): def get_label(entity): """Returns the label of an entity.""" + # pylint: disable=too-many-return-statements onto = entity.namespace.ontology for la in onto.label_annotations: try: label = entity[la] if label: - print("*** LABEL:", repr(label)) - return label + return label.first() except (NoSuchLabelError, AttributeError, TypeError): continue if hasattr(entity, "prefLabel") and entity.prefLabel: @@ -127,7 +127,7 @@ def getiriname(iri): return res.fragment if res.fragment else res.path.rsplit("/", 1)[-1] -def asstring( # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements +def asstring( expr, link="{label}", recursion_depth=0, @@ -154,6 +154,7 @@ def asstring( # pylint: disable=too-many-return-statements,too-many-branches,to Returns: String representation of `expr`. """ + # pylint: disable=too-many-return-statements,too-many-branches,too-many-statements if ontology is None: ontology = expr.ontology diff --git a/tests/test_ontodoc2.py b/tests/test_ontodoc_rst.py similarity index 69% rename from tests/test_ontodoc2.py rename to tests/test_ontodoc_rst.py index 05439dbba..edbf98d7d 100644 --- a/tests/test_ontodoc2.py +++ b/tests/test_ontodoc_rst.py @@ -6,12 +6,14 @@ from pathlib import Path from ontopy import get_ontology - from ontopy.ontodoc2 import OntologyDocumentation + from ontopy.ontodoc_rst import OntologyDocumentation import owlready2 # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() emmo = get_ontology("../EMMO/emmo.ttl").load() # emmo.sync_reasoner(include_imported=True) - od = OntologyDocumentation(emmo) + od = OntologyDocumentation( + emmo, recursive=True, iri_regex="https://w3id.org/emmo" + ) print(od.get_refdoc()) diff --git a/tools/ontodoc b/tools/ontodoc index d21322d5f..c52ea70ae 100755 --- a/tools/ontodoc +++ b/tools/ontodoc @@ -5,6 +5,7 @@ import os import sys import argparse import subprocess # nosec +from pathlib import Path # Support running from uninstalled package by adding parent dir to sys path rootdir = os.path.abspath( @@ -21,6 +22,7 @@ from ontopy.ontodoc import ( # pylint: disable=import-error get_maxwidth, get_docpp, ) +from ontopy.ontodoc_rst import OntologyDocumentation from ontopy.utils import get_format import owlready2 @@ -35,6 +37,7 @@ def main(argv: list = None): manually / through Python. """ + # pylint: disable=too-many-locals,too-many-statements parser = argparse.ArgumentParser( description="Tool for documenting ontologies.", epilog=( @@ -130,9 +133,9 @@ def main(argv: list = None): "-f", metavar="FORMAT", help=( - 'Output format. May be "md", "simple-html" or any other format ' - "supported by pandoc. By default the format is inferred from " - "--output." + 'Output format. May be "rst", "md", "simple-html" or any other ' + "format supported by pandoc. By default the format is inferred " + "from OUTFILE." ), ) parser.add_argument( @@ -199,6 +202,14 @@ def main(argv: list = None): "debugging)." ), ) + parser.add_argument( + "--iri-regex", + "-r", + metavar="REGEX", + help=( + "Regular expression matching IRIs to include in the documentation." + ), + ) args = parser.parse_args(args=argv) # Append to onto_path @@ -229,35 +240,50 @@ def main(argv: list = None): if args.reasoner: onto.sync_reasoner(reasoner=args.reasoner) - # Instantiate ontodoc instance + # Get output format fmt = get_format(args.outfile, default="html", fmt=args.format) - style = get_style(fmt) - figformat = args.figformat if args.figformat else get_figformat(fmt) - maxwidth = args.max_figwidth if args.max_figwidth else get_maxwidth(fmt) - ontodoc = OntoDoc(onto, style=style) - res = get_docpp( - ontodoc, - args.template, - figdir=args.figdir, - figformat=figformat, - maxwidth=maxwidth, - imported=args.imported, - ) - res.process() - try: - res.write( - args.outfile, - fmt=args.format, - pandoc_option_files=args.pandoc_option_files, - pandoc_options=args.pandoc_options, - genfile=args.genfile, + if fmt == "rst": + # New reStructuredText format + od = OntologyDocumentation( + onto, + recursive=args.imported, + iri_regex=args.iri_regex, + ) + docfile = Path(args.outfile) + indexfile = docfile.with_name("index.rst") + od.write_refdoc(docfile=docfile) + if not indexfile.exists(): + print(f"Generating index template: {indexfile}") + od.write_index_template(indexfile=indexfile, docfile=docfile) + + else: + # Instantiate old ontodoc instance + style = get_style(fmt) + figformat = args.figformat if args.figformat else get_figformat(fmt) + maxwidth = args.max_figwidth if args.max_figwidth else get_maxwidth(fmt) + ontodoc = OntoDoc(onto, style=style) + docpp = get_docpp( + ontodoc, + args.template, + figdir=args.figdir, + figformat=figformat, + maxwidth=maxwidth, + imported=args.imported, ) - except subprocess.CalledProcessError as exc: - sys.exit(exc.returncode) # Exit without traceback on pandoc errors + docpp.process() - return res + try: + docpp.write( + args.outfile, + fmt=args.format, + pandoc_option_files=args.pandoc_option_files, + pandoc_options=args.pandoc_options, + genfile=args.genfile, + ) + except subprocess.CalledProcessError as exc: + sys.exit(exc.returncode) # Exit without traceback on pandoc errors if __name__ == "__main__": - docpp = main() + main() From bb0da108f3bbd48de1be1a9c144a736bed541f45 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 12:10:11 +0200 Subject: [PATCH 08/24] Added test including a simple ontology to document. --- ontopy/__init__.py | 2 +- ontopy/ontodoc_rst.py | 107 +++++++++++++++++++++++++++----- ontopy/utils.py | 47 +++++++++++--- tests/testonto/animal.ttl | 100 +++++++++++++++++++++++++++++ tests/testonto/catalog-v001.xml | 17 ++--- tests/testonto/mammal.ttl | 73 ++++++++++++++++++++++ tests/tools/test_ontodoc.py | 17 +++++ tools/ontodoc | 4 ++ 8 files changed, 332 insertions(+), 35 deletions(-) create mode 100644 tests/testonto/animal.ttl create mode 100644 tests/testonto/mammal.ttl diff --git a/ontopy/__init__.py b/ontopy/__init__.py index 2507fca67..fae242c59 100644 --- a/ontopy/__init__.py +++ b/ontopy/__init__.py @@ -23,4 +23,4 @@ from owlready2 import onto_path -__all__ = ("patch", "World", "get_ontology", "onto_path") +__all__ = ("__version__", "World", "get_ontology", "onto_path") diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index d6104452f..700ff1f09 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -1,16 +1,17 @@ -# -*- coding: utf-8 -*- """ A module for documenting ontologies. """ -# pylint: disable=fixme,too-many-lines,no-member + +# pylint: disable=fixme,too-many-lines,no-member,too-many-instance-attributes import re +import time import warnings from html import escape from pathlib import Path from typing import TYPE_CHECKING import rdflib -from rdflib import DCTERMS, URIRef +from rdflib import DCTERMS, OWL, URIRef from ontopy.ontology import Ontology, get_ontology from ontopy.utils import get_label @@ -39,8 +40,6 @@ class ModuleDocumentation: should match. """ - # pylint: disable=too-many-instance-attributes - def __init__( self, ontology: "Optional[Ontology]" = None, @@ -114,13 +113,13 @@ def add_ontology( def get_header(self) -> str: """Return a the reStructuredText header as a string.""" + iri = self.ontology.base_iri.rstrip("#/") if self.title: title = self.title elif self.ontology: - iri = self.ontology.base_iri.rstrip("#/") title = self.graph.value(URIRef(iri), DCTERMS.title) if not title: - title = self.ontology.name + title = iri.rsplit("/", 1)[-1] heading = f"Module: {title}" return f""" @@ -179,10 +178,10 @@ def add_keyvalue(key, value): lines.extend( [ " ", - ' ' + ' ' f'' f"{key.title()}", - f' {escaped}', + f' {escaped}', " ", ] ) @@ -210,12 +209,20 @@ def add_keyvalue(key, value): "", f"* {entity.iri}", "", - ".. raw:: html", - "", ] ) if hasattr(entity, "get_annotations"): - lines.append(' ') + lines.extend( + [ + ".. raw:: html", + "", + '
', + " ", + ' ", + " ", + ] + ) for key, value in entity.get_annotations().items(): if isinstance(value, list): for val in value: @@ -290,12 +297,15 @@ def get_header(self) -> str: ========== """ - def get_refdoc(self, header: bool = True) -> str: + def get_refdoc(self, header: bool = True, subsections: str = "all") -> str: """Return reference documentation of all module entities. Arguments: header: Whether to also include the header in the returned documentation. + subsections: Comma-separated list of subsections to include in + the returned documentation. See ModuleDocumentation.get_refdoc() + for more info. Returns: String with reference documentation. @@ -304,7 +314,7 @@ def get_refdoc(self, header: bool = True) -> str: if header: moduledocs.append(self.get_header()) moduledocs.extend( - md.get_refdoc() + md.get_refdoc(subsections=subsections) for md in self.module_documentations if md.nonempty() ) @@ -314,16 +324,21 @@ def top_ontology(self) -> Ontology: """Return the top-level ontology.""" return self.module_documentations[0].ontology - def write_refdoc(self, docfile=None): + def write_refdoc(self, docfile=None, subsections="all"): """Write reference documentation to disk. Arguments: docfile: Name of file to write to. Defaults to the name of the top ontology with extension `.rst`. + subsections: Comma-separated list of subsections to include in + the returned documentation. See ModuleDocumentation.get_refdoc() + for more info. """ if not docfile: docfile = self.top_ontology().name + ".rst" - Path(docfile).write_text(self.get_refdoc(), encoding="utf8") + Path(docfile).write_text( + self.get_refdoc(subsections=subsections), encoding="utf8" + ) def write_index_template( self, indexfile="index.rst", docfile=None, overwrite=False @@ -351,3 +366,63 @@ def write_index_template( return outpath.write_text(content, encoding="utf8") + + def write_conf_template( + self, conffile="conf.py", docfile=None, overwrite=False + ): + """Write basic template sphinx conf.py file to disk. + + Arguments: + conffile: Name of configuration file to write. + docfile: Name of generated documentation file. If not given, + the name of the top ontology will be used. + overwrite: Whether to overwrite an existing file. + """ + # pylint: disable=redefined-builtin + md = self.module_documentations[0] + + iri = md.ontology.base_iri.rstrip("#/") + authors = list(md.graph.objects(URIRef(iri), DCTERMS.creator)) + license = md.graph.value(URIRef(iri), DCTERMS.license, default=None) + release = md.graph.value(URIRef(iri), OWL.versionInfo, default="1.0") + + author = ", ".join(str(authors)) if authors else "" + copyright = license if license else f"{time.strftime('%Y')}, {author}" + + content = f""" +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +project = '{md.ontology.name}' +copyright = '{copyright}' +author = '{author}' +release = '{release}' + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration + +extensions = [] + +templates_path = ['_templates'] +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = 'alabaster' +html_static_path = ['_static'] +""" + if not conffile: + conffile = Path(docfile).with_name("conf.py") + if overwrite and conffile.exists(): + warnings.warn(f"conf.py file already exists: {conffile}") + return + + conffile.write_text(content, encoding="utf8") diff --git a/ontopy/utils.py b/ontopy/utils.py index 54ba6e301..a731599df 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -27,6 +27,9 @@ from typing import Optional, Union +# Preferred language +PREFERRED_LANGUAGE = "en" + # Format mappings: file extension -> rdflib format name FMAP = { "": "turtle", @@ -94,21 +97,45 @@ def isinteractive(): ) +def get_preferred_language(langstrings: list) -> str: + """Given a list of localised strings, return the one for the + preferred language. If no one match the preferred language, + return the one with no language tag or fallback to the first + string. + + The preferred language is stored as a module variable. You can + change it with: + + >>> import ontopy.utils + >>> ontopy.utils.PREFERRED_LANGUAGE = "it" + + """ + for langstr in langstrings: + if hasattr(langstr, "lang") and langstr.lang == PREFERRED_LANGUAGE: + return langstr + for langstr in langstrings: + if not hasattr(langstr, "lang"): + return langstr + return langstr[0] + + def get_label(entity): """Returns the label of an entity.""" # pylint: disable=too-many-return-statements - onto = entity.namespace.ontology - for la in onto.label_annotations: - try: - label = entity[la] - if label: - return label.first() - except (NoSuchLabelError, AttributeError, TypeError): - continue + if hasattr(entity, "namespace"): + onto = entity.namespace.ontology + if onto.label_annotations: + for la in onto.label_annotations: + try: + label = entity[la] + if label: + return get_preferred_language(label) + except (NoSuchLabelError, AttributeError, TypeError): + continue if hasattr(entity, "prefLabel") and entity.prefLabel: - return entity.prefLabel.first() + return get_preferred_language(entity.prefLabel) if hasattr(entity, "label") and entity.label: - return entity.label.first() + return get_preferred_language(entity.label) if hasattr(entity, "__name__"): return entity.__name__ if hasattr(entity, "name"): diff --git a/tests/testonto/animal.ttl b/tests/testonto/animal.ttl new file mode 100644 index 000000000..d9bc97272 --- /dev/null +++ b/tests/testonto/animal.ttl @@ -0,0 +1,100 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix skos: . +@base . + + rdf:type owl:Ontology ; + owl:versionIRI ; + owl:versionInfo "0.1" . + +################################################################# +# Annotation properties +################################################################# + +### http://www.w3.org/1999/02/22-rdf-syntax-ns#comment +rdf:comment rdf:type owl:AnnotationProperty . + + +### http://www.w3.org/2004/02/skos/core#prefLabel +skos:prefLabel rdf:type owl:AnnotationProperty . + + +### https://w3id.org/emmo/animal#latinName +:latinName rdf:type owl:AnnotationProperty ; + skos:prefLabel "latinName" ; + rdfs:subPropertyOf rdf:comment . + + +################################################################# +# Object Properties +################################################################# + +### https://w3id.org/emmo/animal#chasing +:chasing rdf:type owl:ObjectProperty ; + rdfs:domain :Animal ; + rdfs:range :Animal ; + skos:prefLabel "chasing"@en . + + +################################################################# +# Data properties +################################################################# + +### https://w3id.org/emmo/animal#hasLegs +:hasLegs rdf:type owl:DatatypeProperty ; + rdfs:domain :Animal ; + rdfs:range xsd:int ; + rdf:comment "Number of legs."@en ; + skos:prefLabel "hasLegs"@en . + + +################################################################# +# Classes +################################################################# + +### https://w3id.org/emmo/animal#Animal +:Animal rdf:type owl:Class ; + rdfs:subClassOf owl:Thing ; + skos:prefLabel "Animal"@en ; + :latinName "Animalia" . + + +### https://w3id.org/emmo/animal#FourLeggedAnimal +:FourLeggedAnimal rdf:type owl:Class ; + owl:equivalentClass [ owl:intersectionOf ( :Animal + [ rdf:type owl:Restriction ; + owl:onProperty :hasLegs ; + owl:hasValue "4"^^xsd:int + ] + ) ; + rdf:type owl:Class + ] ; + skos:prefLabel "FourLeggedAnimal"@en ; + :latinName "quadruped" . + + +### https://w3id.org/emmo/animal#Insect +:Insect rdf:type owl:Class ; + rdfs:subClassOf :Animal , + [ rdf:type owl:Restriction ; + owl:onProperty :hasLegs ; + owl:hasValue "6"^^xsd:int + ] ; + skos:prefLabel "Insect"@en , + "Insekt"@no ; + :latinName "Insectum" . + + +### https://w3id.org/emmo/animal#Vertebrates +:Vertebrates rdf:type owl:Class ; + rdfs:subClassOf :Animal ; + skos:prefLabel "Vertebrates"@en , + "Virveldyr"@no ; + :latinName "Vertebrata" . + + +### Generated by the OWL API (version 4.5.26.2023-07-17T20:34:13Z) https://github.com/owlcs/owlapi diff --git a/tests/testonto/catalog-v001.xml b/tests/testonto/catalog-v001.xml index acc7f7926..30e2df245 100644 --- a/tests/testonto/catalog-v001.xml +++ b/tests/testonto/catalog-v001.xml @@ -1,11 +1,12 @@ - - - - - - - - + + + + + + + + + diff --git a/tests/testonto/mammal.ttl b/tests/testonto/mammal.ttl new file mode 100644 index 000000000..5bc5937d3 --- /dev/null +++ b/tests/testonto/mammal.ttl @@ -0,0 +1,73 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix skos: . +@prefix animal: . +@base . + + rdf:type owl:Ontology ; + owl:versionIRI ; + owl:imports ; + owl:versionInfo "0.1" . + +################################################################# +# Classes +################################################################# + +### https://w3id.org/emmo/mammal#Cat +:Cat rdf:type owl:Class ; + rdfs:subClassOf :Felines ; + skos:prefLabel "Cat"@en . + + +### https://w3id.org/emmo/mammal#Felines +:Felines rdf:type owl:Class ; + rdfs:subClassOf animal:FourLeggedAnimal , + :Mammal ; + skos:prefLabel "Felines"@en , + "Kattedyr"@no . + + +### https://w3id.org/emmo/mammal#Mammal +:Mammal rdf:type owl:Class ; + rdfs:subClassOf animal:Vertebrates ; + skos:prefLabel "Mammal"@en , + "Pattedyr"@no ; + animal:latinName "Mammalia" . + + +### https://w3id.org/emmo/mammal#Mouse +:Mouse rdf:type owl:Class ; + rdfs:subClassOf :Rodent ; + skos:prefLabel "Mouse"@en . + + +### https://w3id.org/emmo/mammal#Rodent +:Rodent rdf:type owl:Class ; + rdfs:subClassOf animal:FourLeggedAnimal , + :Mammal ; + skos:prefLabel "Gnager"@no , + "Rodent"@en . + + +################################################################# +# Individuals +################################################################# + +### https://w3id.org/emmo/mammal#Jerry +:Jerry rdf:type owl:NamedIndividual , + :Mouse ; + skos:prefLabel "Jerry"@en . + + +### https://w3id.org/emmo/mammal#Tom +:Tom rdf:type owl:NamedIndividual , + :Cat ; + animal:chasing :Jerry ; + skos:prefLabel "Tom"@en . + + +### Generated by the OWL API (version 4.5.26.2023-07-17T20:34:13Z) https://github.com/owlcs/owlapi diff --git a/tests/tools/test_ontodoc.py b/tests/tools/test_ontodoc.py index 335cf310e..faaa5107b 100644 --- a/tests/tools/test_ontodoc.py +++ b/tests/tools/test_ontodoc.py @@ -48,3 +48,20 @@ def test_run_w_punning() -> None: ontodoc.main( [str(test_file), "--format=simple-html", str(outdir / "test.html")] ) + + +def test_ontodoc_rst() -> None: + """Test reStructuredText output with ontodoc.""" + from ontopy.testutils import ontodir, outdir, get_tool_module + + test_file = ontodir / "mammal.ttl" + ontodoc = get_tool_module("ontodoc") + + ontodoc.main( + [ + "--imported", + "--reasoner=HermiT", + str(test_file), + str(outdir / "mammal.rst"), + ] + ) diff --git a/tools/ontodoc b/tools/ontodoc index c52ea70ae..2a5f34ad6 100755 --- a/tools/ontodoc +++ b/tools/ontodoc @@ -252,10 +252,14 @@ def main(argv: list = None): ) docfile = Path(args.outfile) indexfile = docfile.with_name("index.rst") + conffile = docfile.with_name("conf.py") od.write_refdoc(docfile=docfile) if not indexfile.exists(): print(f"Generating index template: {indexfile}") od.write_index_template(indexfile=indexfile, docfile=docfile) + if not conffile.exists(): + print(f"Generating configuration template: {conffile}") + od.write_conf_template(conffile=conffile, docfile=docfile) else: # Instantiate old ontodoc instance From f1f688d186b256db0972b1d928c174b00f456959 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 13:30:39 +0200 Subject: [PATCH 09/24] Added formal description to ontodoc_rst output --- ontopy/ontodoc_rst.py | 47 +++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index 700ff1f09..7b36f0a4b 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -14,7 +14,7 @@ from rdflib import DCTERMS, OWL, URIRef from ontopy.ontology import Ontology, get_ontology -from ontopy.utils import get_label +from ontopy.utils import asstring, get_label import owlready2 # pylint: disable=wrong-import-order @@ -134,6 +134,7 @@ def get_refdoc( subsections: str = "all", header: bool = True, ) -> str: + # pylint: disable=too-many-branches """Return reference documentation of all module entities. Arguments: @@ -172,16 +173,29 @@ def get_refdoc( if header: lines.append(self.get_header()) + def add_header(name): + clsname = f"element-table-{name.lower().replace(' ', '-')}" + lines.extend( + [ + " ", + f' ', + " ", + ] + ) + def add_keyvalue(key, value): """Help function for adding a key-value row to table.""" escaped = escape(str(value)).replace("\n", "
") + expanded = re.sub( + r"(https?://[^\s]+)", r'\1', escaped + ) lines.extend( [ " ", ' ", - f' ', + f' ', " ", ] ) @@ -207,29 +221,32 @@ def add_keyvalue(key, value): f"{label}", "-" * len(label), "", - f"* {entity.iri}", + ".. raw:: html", "", + '
' + "Annotations
{name}
' f'' f"{key.title()}{escaped}{expanded}
', ] ) + add_keyvalue("IRI", entity.iri) if hasattr(entity, "get_annotations"): - lines.extend( - [ - ".. raw:: html", - "", - '
', - " ", - ' ", - " ", - ] - ) + add_header("Annotatins") for key, value in entity.get_annotations().items(): if isinstance(value, list): for val in value: add_keyvalue(key, val) else: add_keyvalue(key, value) - lines.extend(["
' - "Annotations
", ""]) + if entity.is_a or entity.equivalent_to: + add_header("Formal description") + for r in entity.equivalent_to: + add_keyvalue( + "Equivalent To", asstring(r, ontology=self.ontology) + ) + for r in entity.is_a: + add_keyvalue( + "Subclass Of", asstring(r, ontology=self.ontology) + ) + + lines.extend([" ", ""]) return "\n".join(lines) From d600e51a88827662036b91ca0e014fa761ad9341 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 15:53:32 +0200 Subject: [PATCH 10/24] Added internal links to formal description. --- ontopy/ontodoc_rst.py | 42 +++++++++++++++++++++++++++++-------- tests/tools/test_ontodoc.py | 1 + 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index 7b36f0a4b..cf8f7d6fd 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -3,10 +3,10 @@ """ # pylint: disable=fixme,too-many-lines,no-member,too-many-instance-attributes +import html import re import time import warnings -from html import escape from pathlib import Path from typing import TYPE_CHECKING @@ -183,19 +183,22 @@ def add_header(name): ] ) - def add_keyvalue(key, value): + def add_keyvalue(key, value, escape=True, htmllink=True): """Help function for adding a key-value row to table.""" - escaped = escape(str(value)).replace("\n", "
") - expanded = re.sub( - r"(https?://[^\s]+)", r'\1', escaped - ) + if escape: + value = html.escape(str(value)) + if htmllink: + value = re.sub( + r"(https?://[^\s]+)", r'\1', value + ) + value = value.replace("\n", "
") lines.extend( [ " ", ' ' f'' f"{key.title()}", - f' {expanded}', + f' {value}', " ", ] ) @@ -238,12 +241,33 @@ def add_keyvalue(key, value): if entity.is_a or entity.equivalent_to: add_header("Formal description") for r in entity.equivalent_to: + + # FIXME: Skip restrictions with value None to work + # around bug in Owlready2 that doesn't handle custom + # datatypes in restrictions correctly... + if hasattr(r, "value") and r.value is None: + continue + add_keyvalue( - "Equivalent To", asstring(r, ontology=self.ontology) + "Equivalent To", + asstring( + r, + link='{label}', + ontology=self.ontology, + ), + escape=False, + htmllink=False, ) for r in entity.is_a: add_keyvalue( - "Subclass Of", asstring(r, ontology=self.ontology) + "Subclass Of", + asstring( + r, + link='{label}', + ontology=self.ontology, + ), + escape=False, + htmllink=False, ) lines.extend([" ", ""]) diff --git a/tests/tools/test_ontodoc.py b/tests/tools/test_ontodoc.py index faaa5107b..e24a0a2c4 100644 --- a/tests/tools/test_ontodoc.py +++ b/tests/tools/test_ontodoc.py @@ -61,6 +61,7 @@ def test_ontodoc_rst() -> None: [ "--imported", "--reasoner=HermiT", + "--iri-regex=^https://w3id.org/emmo/domain", str(test_file), str(outdir / "mammal.rst"), ] From 7066c2d88c02f22248b978eaef7f89fd1a225dea Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 16:14:38 +0200 Subject: [PATCH 11/24] Cleaned up test_ontodoc_rst.py --- tests/test_ontodoc_rst.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_ontodoc_rst.py b/tests/test_ontodoc_rst.py index edbf98d7d..42fa0ae99 100644 --- a/tests/test_ontodoc_rst.py +++ b/tests/test_ontodoc_rst.py @@ -1,7 +1,8 @@ """Test ontodoc""" -if True: - # def test_ontodoc(): + +# if True: +def test_ontodoc(): """Test ontodoc.""" from pathlib import Path @@ -9,8 +10,8 @@ from ontopy.ontodoc_rst import OntologyDocumentation import owlready2 - # emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() - emmo = get_ontology("../EMMO/emmo.ttl").load() + emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() + # emmo = get_ontology("../EMMO/emmo.ttl").load() # emmo.sync_reasoner(include_imported=True) od = OntologyDocumentation( From 98a9ff1371b857a088720f3793805b5729a5a573 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 17:41:04 +0200 Subject: [PATCH 12/24] Fixed test_patch --- tests/ontopy_tests/test_patch.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index 24185b2ee..1712e4c65 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -18,17 +18,16 @@ def test_get_by_label_onto(emmo: "Ontology") -> None: assert emmo.Atom.get_parents() == {emmo.MolecularEntity} - setassert( - emmo.Atom.get_annotations().keys(), - { - "prefLabel", - "altLabel", - "elucidation", - "comment", - }, - ) - setassert( - emmo.Atom.get_annotations(all=True).keys(), + annot = set(str(a) for a in emmo.Atom.get_annotations().keys()) + assert annot == { + "prefLabel", + "altLabel", + "elucidation", + "comment", + } + + annot = set(str(a) for a in emmo.Atom.get_annotations(all=True).keys()) + assert not annot.difference( { "qualifiedCardinality", "minQualifiedCardinality", @@ -44,6 +43,7 @@ def test_get_by_label_onto(emmo: "Ontology") -> None: "conceptualisation", "logo", "comment", + "label", "dbpediaReference", "definition", "VIMTerm", From 2f7f9d8a1bf52eb72b047c26b74b0edd3bfbb16e Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sun, 7 Apr 2024 19:58:30 +0200 Subject: [PATCH 13/24] Fixed creating author list --- ontopy/ontodoc_rst.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index cf8f7d6fd..1a864caa3 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -423,11 +423,11 @@ def write_conf_template( md = self.module_documentations[0] iri = md.ontology.base_iri.rstrip("#/") - authors = list(md.graph.objects(URIRef(iri), DCTERMS.creator)) + authors = sorted(md.graph.objects(URIRef(iri), DCTERMS.creator)) license = md.graph.value(URIRef(iri), DCTERMS.license, default=None) release = md.graph.value(URIRef(iri), OWL.versionInfo, default="1.0") - author = ", ".join(str(authors)) if authors else "" + author = ", ".join(a.value for a in authors) if authors else "" copyright = license if license else f"{time.strftime('%Y')}, {author}" content = f""" From 4f4b47a6ee64b38e8f9e4d3208274d89e0b8b101 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 11 Apr 2024 22:15:25 +0200 Subject: [PATCH 14/24] Fixed typo --- ontopy/ontodoc_rst.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index 1a864caa3..fbc1384fd 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -111,8 +111,8 @@ def add_ontology( for entity in ontology.get_entities(imported=imported): self.add_entity(entity) - def get_header(self) -> str: - """Return a the reStructuredText header as a string.""" + def get_title(self) -> str: + """Return a module title.""" iri = self.ontology.base_iri.rstrip("#/") if self.title: title = self.title @@ -120,8 +120,11 @@ def get_header(self) -> str: title = self.graph.value(URIRef(iri), DCTERMS.title) if not title: title = iri.rsplit("/", 1)[-1] + return title - heading = f"Module: {title}" + def get_header(self) -> str: + """Return a the reStructuredText header as a string.""" + heading = f"Module: {self.get_title()}" return f""" {heading.title()} @@ -134,7 +137,7 @@ def get_refdoc( subsections: str = "all", header: bool = True, ) -> str: - # pylint: disable=too-many-branches + # pylint: disable=too-many-branches,too-many-locals """Return reference documentation of all module entities. Arguments: @@ -178,7 +181,7 @@ def add_header(name): lines.extend( [ " ", - f' {name}', + f' {name}', " ", ] ) @@ -205,9 +208,13 @@ def add_keyvalue(key, value, escape=True, htmllink=True): for subsection in subsections.split(","): if maps[subsection]: + moduletitle = self.get_title().lower().replace(" ", "-") + anchor = f"{moduletitle}-{subsection.replace('_', '-')}" lines.extend( [ - "-" * len(subsection), + "", + f".. _{anchor}:", + "", subsection.replace("_", " ").title(), "-" * len(subsection), "", @@ -222,7 +229,7 @@ def add_keyvalue(key, value, escape=True, htmllink=True): f'
', "", f"{label}", - "-" * len(label), + "^" * len(label), "", ".. raw:: html", "", @@ -305,7 +312,7 @@ def __init__( self.module_documentations = [] # Explicitly included ontologies - included_ontologies = [] + included_ontologies = {} for onto in ontologies: if isinstance(onto, (str, Path)): onto = get_ontology(onto).load() @@ -314,18 +321,18 @@ def __init__( "expected ontology as an IRI, Path or Ontology object, " f"got: {onto}" ) - if onto not in included_ontologies: - included_ontologies.append(onto) + if onto.base_iri not in included_ontologies: + included_ontologies[onto.base_iri] = onto # Indirectly included ontologies (imported) if imported: - for onto in included_ontologies[:]: + for onto in list(included_ontologies.values()): for o in onto.get_imported_ontologies(recursive=recursive): - if o not in included_ontologies: - included_ontologies.append(o) + if o.base_iri not in included_ontologies: + included_ontologies[o.base_iri] = o # Module documentations - for onto in included_ontologies: + for onto in included_ontologies.values(): self.module_documentations.append( ModuleDocumentation(onto, iri_regex=iri_regex) ) From b3e8f4a7c9485973dd79fe91c8b4fc19120842ef Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 11 Apr 2024 22:23:51 +0200 Subject: [PATCH 15/24] Corrected spelling error --- ontopy/ontodoc_rst.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index fbc1384fd..24c6c0bce 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -238,7 +238,7 @@ def add_keyvalue(key, value, escape=True, htmllink=True): ) add_keyvalue("IRI", entity.iri) if hasattr(entity, "get_annotations"): - add_header("Annotatins") + add_header("Annotations") for key, value in entity.get_annotations().items(): if isinstance(value, list): for val in value: From 0fd965f90f6fa1d3057b05a4f88c6ef5f5257787 Mon Sep 17 00:00:00 2001 From: francescalb Date: Mon, 15 Apr 2024 15:30:45 +0200 Subject: [PATCH 16/24] Fixed global variable in doc-string to be same as default. If not the later tests fail (since a global variable is changed) --- ontopy/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/utils.py b/ontopy/utils.py index a731599df..866222204 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -107,7 +107,7 @@ def get_preferred_language(langstrings: list) -> str: change it with: >>> import ontopy.utils - >>> ontopy.utils.PREFERRED_LANGUAGE = "it" + >>> ontopy.utils.PREFERRED_LANGUAGE = "en" """ for langstr in langstrings: From 82e31ba538ed56ae7df44f8722a3061a00f999ce Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Tue, 30 Apr 2024 23:10:06 +0200 Subject: [PATCH 17/24] Fixed typo --- ontopy/ontodoc_rst.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/ontodoc_rst.py b/ontopy/ontodoc_rst.py index 24c6c0bce..20ac86a5f 100644 --- a/ontopy/ontodoc_rst.py +++ b/ontopy/ontodoc_rst.py @@ -181,7 +181,7 @@ def add_header(name): lines.extend( [ " ", - f' {name}', + f' {name}', " ", ] ) From 8b549f1c0109a1a6eedcf47d49dce290b823f10d Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 1 May 2024 11:16:55 +0200 Subject: [PATCH 18/24] Fixed failing test_save in master After simplifying paths in test_save_emmo_domain_ontology() in test_save.py to make it possible to debug, the test works again :-) --- tests/test_save.py | 24 +++++------------------- 1 file changed, 5 insertions(+), 19 deletions(-) diff --git a/tests/test_save.py b/tests/test_save.py index eeb9ca17b..f93bf7cd2 100755 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -195,24 +195,12 @@ def test_save_and_copy_emmo( assert copied_emmo == emmo -def test_save_emmo_domain_ontology( - tmpdir: "Path", - repo_dir: "Path", -) -> None: +def test_save_emmo_domain_ontology() -> None: import os from pathlib import Path from ontopy.utils import directory_layout from ontopy import get_ontology - - # For debugging purposes tmpdir can be set to a directory - # in the current directory: test_save_dir - # Remember to remove the directory after testing - debug = True - if debug: - tmpdir = repo_dir / "tests" / "test_save_dir" - import os - - os.makedirs(tmpdir, exist_ok=True) + from ontopy.testutils import ontodir, outdir # This test was created with the domain-electrochemistry ontology which imports # emmo submodules as well as chameo. @@ -221,11 +209,9 @@ def test_save_emmo_domain_ontology( # while emmo and chameo start with https://w3id.org/emmo/ # For faster tests a dummyontology was created. # onto = get_ontology('https://raw.githubusercontent.com/emmo-repo/domain-electrochemistry/master/electrochemistry.ttl').load() - onto = get_ontology( - repo_dir / "tests" / "testonto" / "dummyonto_w_dummyemmo.ttl" - ).load() + onto = get_ontology(ontodir / "dummyonto_w_dummyemmo.ttl").load() - outputdir = tmpdir / "saved_emmo_domain_ontology" + outputdir = outdir / "saved_emmo_domain_ontology" savedfile = onto.save( format="rdfxml", dir=outputdir, @@ -247,7 +233,7 @@ def test_save_emmo_domain_ontology( } # Test saving but giving filename. It should then be saved in the parent directory - outputdir2 = tmpdir / "saved_emmo_domain_ontology2" + outputdir2 = outdir / "saved_emmo_domain_ontology2" savedfile2 = onto.save( format="rdfxml", dir=outputdir2, From 5b1ce0e1aa32f9b95d852ed4081b833d18d59f72 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 1 May 2024 22:07:03 +0200 Subject: [PATCH 19/24] Fixed test_save --- tests/test_save.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/test_save.py b/tests/test_save.py index f93bf7cd2..de70f8154 100755 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -224,9 +224,9 @@ def test_save_emmo_domain_ontology() -> None: assert set( os.listdir(outputdir / "emmo.info" / "emmo" / "domain" / "chameo") ) == {"chameo.rdfxml", "catalog-v001.xml"} - assert set( + assert not {"isq.rdfxml", "catalog-v001.xml"}.difference( os.listdir(outputdir / "emmo.info" / "emmo" / "disciplines") - ) == {"isq.rdfxml", "catalog-v001.xml"} + ) assert set(os.listdir(outputdir / "w3id.org" / "emmo" / "domain")) == { "dummyonto.rdfxml", "catalog-v001.xml", @@ -249,8 +249,8 @@ def test_save_emmo_domain_ontology() -> None: "catalog-v001.xml", } assert set( - os.listdir(outputdir / "emmo.info" / "emmo" / "domain" / "chameo") + os.listdir(outputdir2 / "emmo.info" / "emmo" / "domain" / "chameo") ) == {"chameo.rdfxml", "catalog-v001.xml"} - assert set( - os.listdir(outputdir / "emmo.info" / "emmo" / "disciplines") - ) == {"isq.rdfxml", "catalog-v001.xml"} + assert not {"isq.rdfxml", "catalog-v001.xml"}.difference( + os.listdir(outputdir2 / "emmo.info" / "emmo" / "disciplines") + ) From ba7e997de96c25743bacf45658e5c9e3d6a9322a Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 9 May 2024 09:07:15 +0200 Subject: [PATCH 20/24] Added test for content of generated file as well --- tests/tools/test_ontodoc.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/tools/test_ontodoc.py b/tests/tools/test_ontodoc.py index e24a0a2c4..19e182c8a 100644 --- a/tests/tools/test_ontodoc.py +++ b/tests/tools/test_ontodoc.py @@ -54,15 +54,17 @@ def test_ontodoc_rst() -> None: """Test reStructuredText output with ontodoc.""" from ontopy.testutils import ontodir, outdir, get_tool_module - test_file = ontodir / "mammal.ttl" ontodoc = get_tool_module("ontodoc") - ontodoc.main( [ "--imported", "--reasoner=HermiT", "--iri-regex=^https://w3id.org/emmo/domain", - str(test_file), + str(ontodir / "mammal.ttl"), str(outdir / "mammal.rst"), ] ) + rstfile = outdir / "mammal.rst" + assert rstfile.exists() + content = rstfile.read_text() + assert "latinName" in content From 587560662557b8240c8240888837d6ab7c77bcf0 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 9 May 2024 10:36:59 +0200 Subject: [PATCH 21/24] Replaced EMMO with local animal ontology --- tests/test_ontodoc_rst.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/test_ontodoc_rst.py b/tests/test_ontodoc_rst.py index 42fa0ae99..06f6b5b0c 100644 --- a/tests/test_ontodoc_rst.py +++ b/tests/test_ontodoc_rst.py @@ -8,13 +8,14 @@ def test_ontodoc(): from ontopy import get_ontology from ontopy.ontodoc_rst import OntologyDocumentation + from ontopy.testutils import ontodir import owlready2 - emmo = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() - # emmo = get_ontology("../EMMO/emmo.ttl").load() - # emmo.sync_reasoner(include_imported=True) + # onto = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() + onto = get_ontology(ontodir / "animal.ttl").load() + # onto.sync_reasoner(include_imported=True) od = OntologyDocumentation( - emmo, recursive=True, iri_regex="https://w3id.org/emmo" + onto, recursive=True, iri_regex="https://w3id.org/emmo" ) print(od.get_refdoc()) From 3103e17144ab7c2cf6d752fea3f3c5e328c98e90 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Thu, 9 May 2024 11:11:49 +0200 Subject: [PATCH 22/24] Added test for get_preferred_language() --- ontopy/utils.py | 17 ++++++++++------- tests/ontopy_tests/test_utils.py | 13 +++++++++++++ tests/test_ontodoc_rst.py | 2 +- tests/testonto/animal.ttl | 28 ++++++++++++++-------------- 4 files changed, 38 insertions(+), 22 deletions(-) diff --git a/ontopy/utils.py b/ontopy/utils.py index 866222204..aba32af77 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -97,10 +97,11 @@ def isinteractive(): ) -def get_preferred_language(langstrings: list) -> str: - """Given a list of localised strings, return the one for the - preferred language. If no one match the preferred language, - return the one with no language tag or fallback to the first +def get_preferred_language(langstrings: list, lang=None) -> str: + """Given a list of localised strings, return the one in language + `lang`. If `lang` is not given, use + `ontopy.utils.PREFERRED_LANGUAGE`. If no one match is found, + return the first one with no language tag or fallback to the first string. The preferred language is stored as a module variable. You can @@ -110,13 +111,15 @@ def get_preferred_language(langstrings: list) -> str: >>> ontopy.utils.PREFERRED_LANGUAGE = "en" """ + if lang is None: + lang = PREFERRED_LANGUAGE for langstr in langstrings: - if hasattr(langstr, "lang") and langstr.lang == PREFERRED_LANGUAGE: - return langstr + if hasattr(langstr, "lang") and langstr.lang == lang: + return str(langstr) for langstr in langstrings: if not hasattr(langstr, "lang"): return langstr - return langstr[0] + return str(langstrings[0]) def get_label(entity): diff --git a/tests/ontopy_tests/test_utils.py b/tests/ontopy_tests/test_utils.py index 1505ef03f..cb41d775f 100644 --- a/tests/ontopy_tests/test_utils.py +++ b/tests/ontopy_tests/test_utils.py @@ -37,3 +37,16 @@ def test_rename_iris(testonto: "Ontology"): "http://www.w3.org/2004/02/skos/core#exactMatch", "http://emmo.info/models#testclass", ) + + +def test_preferred_language(): + from ontopy import get_ontology + from ontopy.testutils import ontodir + from ontopy.utils import get_preferred_language + + onto = get_ontology(ontodir / "animal.ttl").load() + pl = onto.Vertebrate.prefLabel + assert get_preferred_language(pl) == "Vertebrate" + assert get_preferred_language(pl, "en") == "Vertebrate" + assert get_preferred_language(pl, "no") == "Virveldyr" + assert get_preferred_language(pl, "it") == "Vertebrate" diff --git a/tests/test_ontodoc_rst.py b/tests/test_ontodoc_rst.py index 06f6b5b0c..0267445db 100644 --- a/tests/test_ontodoc_rst.py +++ b/tests/test_ontodoc_rst.py @@ -12,7 +12,7 @@ def test_ontodoc(): import owlready2 # onto = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load() - onto = get_ontology(ontodir / "animal.ttl").load() + onto = get_ontology(ontodir / "mammal.ttl").load() # onto.sync_reasoner(include_imported=True) od = OntologyDocumentation( diff --git a/tests/testonto/animal.ttl b/tests/testonto/animal.ttl index d9bc97272..bb2a9779b 100644 --- a/tests/testonto/animal.ttl +++ b/tests/testonto/animal.ttl @@ -8,8 +8,8 @@ @base . rdf:type owl:Ontology ; - owl:versionIRI ; - owl:versionInfo "0.1" . + owl:versionIRI ; + owl:versionInfo "0.1" . ################################################################# # Annotation properties @@ -23,7 +23,7 @@ rdf:comment rdf:type owl:AnnotationProperty . skos:prefLabel rdf:type owl:AnnotationProperty . -### https://w3id.org/emmo/animal#latinName +### https://w3id.org/emmo/domain/animal#latinName :latinName rdf:type owl:AnnotationProperty ; skos:prefLabel "latinName" ; rdfs:subPropertyOf rdf:comment . @@ -33,7 +33,7 @@ skos:prefLabel rdf:type owl:AnnotationProperty . # Object Properties ################################################################# -### https://w3id.org/emmo/animal#chasing +### https://w3id.org/emmo/domain/animal#chasing :chasing rdf:type owl:ObjectProperty ; rdfs:domain :Animal ; rdfs:range :Animal ; @@ -44,7 +44,7 @@ skos:prefLabel rdf:type owl:AnnotationProperty . # Data properties ################################################################# -### https://w3id.org/emmo/animal#hasLegs +### https://w3id.org/emmo/domain/animal#hasLegs :hasLegs rdf:type owl:DatatypeProperty ; rdfs:domain :Animal ; rdfs:range xsd:int ; @@ -56,14 +56,14 @@ skos:prefLabel rdf:type owl:AnnotationProperty . # Classes ################################################################# -### https://w3id.org/emmo/animal#Animal +### https://w3id.org/emmo/domain/animal#Animal :Animal rdf:type owl:Class ; rdfs:subClassOf owl:Thing ; skos:prefLabel "Animal"@en ; :latinName "Animalia" . -### https://w3id.org/emmo/animal#FourLeggedAnimal +### https://w3id.org/emmo/domain/animal#FourLeggedAnimal :FourLeggedAnimal rdf:type owl:Class ; owl:equivalentClass [ owl:intersectionOf ( :Animal [ rdf:type owl:Restriction ; @@ -77,7 +77,7 @@ skos:prefLabel rdf:type owl:AnnotationProperty . :latinName "quadruped" . -### https://w3id.org/emmo/animal#Insect +### https://w3id.org/emmo/domain/animal#Insect :Insect rdf:type owl:Class ; rdfs:subClassOf :Animal , [ rdf:type owl:Restriction ; @@ -89,12 +89,12 @@ skos:prefLabel rdf:type owl:AnnotationProperty . :latinName "Insectum" . -### https://w3id.org/emmo/animal#Vertebrates -:Vertebrates rdf:type owl:Class ; - rdfs:subClassOf :Animal ; - skos:prefLabel "Vertebrates"@en , - "Virveldyr"@no ; - :latinName "Vertebrata" . +### https://w3id.org/emmo/domain/animal#Vertebrate +:Vertebrate rdf:type owl:Class ; + rdfs:subClassOf :Animal ; + skos:prefLabel "Vertebrate"@en , + "Virveldyr"@no ; + :latinName "Vertebrata" . ### Generated by the OWL API (version 4.5.26.2023-07-17T20:34:13Z) https://github.com/owlcs/owlapi From dfec0c4a358482ea880268e5b4a85c0a3d11a4b9 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 25 May 2024 09:56:56 +0200 Subject: [PATCH 23/24] Updated test as suggested by Francesca --- tests/test_save.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/tests/test_save.py b/tests/test_save.py index de70f8154..be34a3d4d 100755 --- a/tests/test_save.py +++ b/tests/test_save.py @@ -224,14 +224,18 @@ def test_save_emmo_domain_ontology() -> None: assert set( os.listdir(outputdir / "emmo.info" / "emmo" / "domain" / "chameo") ) == {"chameo.rdfxml", "catalog-v001.xml"} - assert not {"isq.rdfxml", "catalog-v001.xml"}.difference( - os.listdir(outputdir / "emmo.info" / "emmo" / "disciplines") - ) + assert set(os.listdir(outputdir / "w3id.org" / "emmo" / "domain")) == { "dummyonto.rdfxml", "catalog-v001.xml", } + created_files = set( + os.listdir(outputdir / "emmo.info" / "emmo" / "disciplines") + ) + for fname in ("isq.rdfxml", "catalog-v001.xml"): + assert fname in created_files + # Test saving but giving filename. It should then be saved in the parent directory outputdir2 = outdir / "saved_emmo_domain_ontology2" savedfile2 = onto.save( @@ -251,6 +255,9 @@ def test_save_emmo_domain_ontology() -> None: assert set( os.listdir(outputdir2 / "emmo.info" / "emmo" / "domain" / "chameo") ) == {"chameo.rdfxml", "catalog-v001.xml"} - assert not {"isq.rdfxml", "catalog-v001.xml"}.difference( + + created_files2 = set( os.listdir(outputdir2 / "emmo.info" / "emmo" / "disciplines") ) + for fname in ("isq.rdfxml", "catalog-v001.xml"): + assert fname in created_files From bf1fc98bc923aec8f0cb5eb7b8225ffdb8a05b8b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 25 May 2024 10:12:58 +0200 Subject: [PATCH 24/24] Got rid of two new pylint errors... --- ontopy/excelparser.py | 2 ++ ontopy/ontology.py | 1 + 2 files changed, 3 insertions(+) diff --git a/ontopy/excelparser.py b/ontopy/excelparser.py index 02cd9c49f..34504ad66 100755 --- a/ontopy/excelparser.py +++ b/ontopy/excelparser.py @@ -652,6 +652,8 @@ def _add_entities( owlready2.DataPropertyClass, ]: rowheader = "subPropertyOf" + else: + raise TypeError(f"Unexpected `entitytype`: {entitytype!r}") # Dictionary with lists of entities that raise errors entities_with_errors = { diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 5eacec242..c1952c535 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -1114,6 +1114,7 @@ def get_imported_ontologies(self, recursive=False): def rec_imported(onto): for ontology in onto.imported_ontologies: + # pylint: disable=possibly-used-before-assignment if ontology not in imported: imported.add(ontology) rec_imported(ontology)