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()