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/__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/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/ontodoc_rst.py b/ontopy/ontodoc_rst.py
new file mode 100644
index 000000000..20ac86a5f
--- /dev/null
+++ b/ontopy/ontodoc_rst.py
@@ -0,0 +1,476 @@
+"""
+A module for documenting ontologies.
+"""
+
+# pylint: disable=fixme,too-many-lines,no-member,too-many-instance-attributes
+import html
+import re
+import time
+import warnings
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+import rdflib
+from rdflib import DCTERMS, OWL, URIRef
+
+from ontopy.ontology import Ontology, get_ontology
+from ontopy.utils import asstring, get_label
+
+import owlready2 # pylint: disable=wrong-import-order
+
+if TYPE_CHECKING:
+ 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]
+
+
+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
+ iri_regex: A regular expression that the IRI of documented entities
+ should match.
+ """
+
+ 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()
+ )
+ self.classes = set()
+ self.object_properties = set()
+ self.data_properties = set()
+ self.annotation_properties = set()
+ self.individuals = set()
+ self.datatypes = set()
+
+ if ontology:
+ self.add_ontology(ontology)
+
+ if entities:
+ 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.
+ """
+ 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):
+ 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 ontology.get_entities(imported=imported):
+ self.add_entity(entity)
+
+ def get_title(self) -> str:
+ """Return a module title."""
+ iri = self.ontology.base_iri.rstrip("#/")
+ if self.title:
+ title = self.title
+ elif self.ontology:
+ title = self.graph.value(URIRef(iri), DCTERMS.title)
+ if not title:
+ title = iri.rsplit("/", 1)[-1]
+ return title
+
+ def get_header(self) -> str:
+ """Return a the reStructuredText header as a string."""
+ heading = f"Module: {self.get_title()}"
+ return f"""
+
+{heading.title()}
+{'='*len(heading)}
+
+"""
+
+ def get_refdoc(
+ self,
+ subsections: str = "all",
+ header: bool = True,
+ ) -> str:
+ # pylint: disable=too-many-branches,too-many-locals
+ """Return reference documentation of all module entities.
+
+ Arguments:
+ subsections: Comma-separated list of subsections to include in
+ 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,
+ "data_properties": self.data_properties,
+ "annotation_properties": self.annotation_properties,
+ "individuals": self.individuals,
+ "datatypes": self.datatypes,
+ }
+ lines = []
+
+ if header:
+ lines.append(self.get_header())
+
+ def add_header(name):
+ clsname = f"element-table-{name.lower().replace(' ', '-')}"
+ lines.extend(
+ [
+ "
",
+ f' {name} | ',
+ "
",
+ ]
+ )
+
+ def add_keyvalue(key, value, escape=True, htmllink=True):
+ """Help function for adding a key-value row to table."""
+ 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' {value} | ',
+ "
",
+ ]
+ )
+
+ for subsection in subsections.split(","):
+ if maps[subsection]:
+ moduletitle = self.get_title().lower().replace(" ", "-")
+ anchor = f"{moduletitle}-{subsection.replace('_', '-')}"
+ lines.extend(
+ [
+ "",
+ f".. _{anchor}:",
+ "",
+ subsection.replace("_", " ").title(),
+ "-" * len(subsection),
+ "",
+ ]
+ )
+ for entity in sorted(maps[subsection], key=get_label):
+ label = get_label(entity)
+ lines.extend(
+ [
+ ".. raw:: html",
+ "",
+ f' ',
+ "",
+ f"{label}",
+ "^" * len(label),
+ "",
+ ".. raw:: html",
+ "",
+ ' ',
+ ]
+ )
+ add_keyvalue("IRI", entity.iri)
+ if hasattr(entity, "get_annotations"):
+ add_header("Annotations")
+ 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)
+ 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,
+ link='{label}',
+ ontology=self.ontology,
+ ),
+ escape=False,
+ htmllink=False,
+ )
+ for r in entity.is_a:
+ add_keyvalue(
+ "Subclass Of",
+ asstring(
+ r,
+ link='{label}',
+ ontology=self.ontology,
+ ),
+ escape=False,
+ htmllink=False,
+ )
+
+ lines.extend(["
", ""])
+
+ return "\n".join(lines)
+
+
+class OntologyDocumentation:
+ """Documentation for an ontology with a common namespace.
+
+ Arguments:
+ ontologies: Ontologies to include in the generated documentation.
+ All entities in these ontologies will be included.
+ imported: Whether to include imported ontologies.
+ recursive: Whether to recursively import all imported ontologies.
+ Implies `recursive=True`.
+ iri_regex: A regular expression that the IRI of documented entities
+ should match.
+ """
+
+ def __init__(
+ self,
+ ontologies: "Iterable[Ontology]",
+ imported: bool = True,
+ recursive: bool = False,
+ iri_regex: "Optional[str]" = None,
+ ) -> None:
+ if isinstance(ontologies, (Ontology, str, Path)):
+ ontologies = [ontologies]
+
+ if recursive:
+ imported = True
+
+ self.iri_regex = iri_regex
+ 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.base_iri not in included_ontologies:
+ included_ontologies[onto.base_iri] = onto
+
+ # Indirectly included ontologies (imported)
+ if imported:
+ for onto in list(included_ontologies.values()):
+ for o in onto.get_imported_ontologies(recursive=recursive):
+ if o.base_iri not in included_ontologies:
+ included_ontologies[o.base_iri] = o
+
+ # Module documentations
+ for onto in included_ontologies.values():
+ self.module_documentations.append(
+ ModuleDocumentation(onto, iri_regex=iri_regex)
+ )
+
+ def get_header(self) -> str:
+ """Return a the reStructuredText header as a string."""
+ return """
+==========
+References
+==========
+"""
+
+ 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.
+ """
+ moduledocs = []
+ if header:
+ moduledocs.append(self.get_header())
+ moduledocs.extend(
+ md.get_refdoc(subsections=subsections)
+ for md in self.module_documentations
+ 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, 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(subsections=subsections), 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")
+
+ 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 = 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(a.value for a in 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/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)
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 010e5e751..aba32af77 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,12 +97,48 @@ def isinteractive():
)
+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
+ change it with:
+
+ >>> import ontopy.utils
+ >>> ontopy.utils.PREFERRED_LANGUAGE = "en"
+
+ """
+ if lang is None:
+ lang = PREFERRED_LANGUAGE
+ for langstr in langstrings:
+ if hasattr(langstr, "lang") and langstr.lang == lang:
+ return str(langstr)
+ for langstr in langstrings:
+ if not hasattr(langstr, "lang"):
+ return langstr
+ return str(langstrings[0])
+
+
def get_label(entity):
"""Returns the label of an entity."""
+ # pylint: disable=too-many-return-statements
+ 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"):
@@ -118,7 +157,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,
@@ -145,6 +184,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/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",
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
new file mode 100644
index 000000000..0267445db
--- /dev/null
+++ b/tests/test_ontodoc_rst.py
@@ -0,0 +1,21 @@
+"""Test ontodoc"""
+
+
+# if True:
+def test_ontodoc():
+ """Test ontodoc."""
+ from pathlib import Path
+
+ from ontopy import get_ontology
+ from ontopy.ontodoc_rst import OntologyDocumentation
+ from ontopy.testutils import ontodir
+ import owlready2
+
+ # onto = get_ontology("https://w3id.org/emmo/1.0.0-rc1").load()
+ onto = get_ontology(ontodir / "mammal.ttl").load()
+ # onto.sync_reasoner(include_imported=True)
+
+ od = OntologyDocumentation(
+ onto, recursive=True, iri_regex="https://w3id.org/emmo"
+ )
+ print(od.get_refdoc())
diff --git a/tests/test_save.py b/tests/test_save.py
index f93bf7cd2..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 set(
- 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",
}
+ 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(
@@ -249,8 +253,11 @@ 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"}
+
+ created_files2 = set(
+ os.listdir(outputdir2 / "emmo.info" / "emmo" / "disciplines")
+ )
+ for fname in ("isq.rdfxml", "catalog-v001.xml"):
+ assert fname in created_files
diff --git a/tests/testonto/animal.ttl b/tests/testonto/animal.ttl
new file mode 100644
index 000000000..bb2a9779b
--- /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/domain/animal#latinName
+:latinName rdf:type owl:AnnotationProperty ;
+ skos:prefLabel "latinName" ;
+ rdfs:subPropertyOf rdf:comment .
+
+
+#################################################################
+# Object Properties
+#################################################################
+
+### https://w3id.org/emmo/domain/animal#chasing
+:chasing rdf:type owl:ObjectProperty ;
+ rdfs:domain :Animal ;
+ rdfs:range :Animal ;
+ skos:prefLabel "chasing"@en .
+
+
+#################################################################
+# Data properties
+#################################################################
+
+### https://w3id.org/emmo/domain/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/domain/animal#Animal
+:Animal rdf:type owl:Class ;
+ rdfs:subClassOf owl:Thing ;
+ skos:prefLabel "Animal"@en ;
+ :latinName "Animalia" .
+
+
+### https://w3id.org/emmo/domain/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/domain/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/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
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..19e182c8a 100644
--- a/tests/tools/test_ontodoc.py
+++ b/tests/tools/test_ontodoc.py
@@ -48,3 +48,23 @@ 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
+
+ ontodoc = get_tool_module("ontodoc")
+ ontodoc.main(
+ [
+ "--imported",
+ "--reasoner=HermiT",
+ "--iri-regex=^https://w3id.org/emmo/domain",
+ str(ontodir / "mammal.ttl"),
+ str(outdir / "mammal.rst"),
+ ]
+ )
+ rstfile = outdir / "mammal.rst"
+ assert rstfile.exists()
+ content = rstfile.read_text()
+ assert "latinName" in content
diff --git a/tools/ontodoc b/tools/ontodoc
index d21322d5f..2a5f34ad6 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,54 @@ 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")
+ 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
+ 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()