ontodoc_rst¶
A module for documenting ontologies.
ModuleDocumentation
¶
Class for documentating a module in an ontology.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ontology |
Optional[Ontology] |
Ontology to include in the generated documentation. All entities in this ontology will be included. |
None |
entities |
Optional[Iterable[Entity]] |
Explicit listing of entities (classes, properties, individuals, datatypes) to document. Normally not needed. |
None |
title |
Optional[str] |
Header title. Be default it is inferred from title of |
None |
iri_regex |
Optional[str] |
A regular expression that the IRI of documented entities should match. |
None |
Source code in ontopy/ontodoc_rst.py
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()
# All navigation IDs added by the ontology. Used to warn about
# dublicated IDs
self.navids = 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.data_properties.add(entity)
elif isinstance(entity, owlready2.AnnotationPropertyClass):
self.annotation_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,too-many-statements
"""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())
annotations_ranked = _get_annotation_rank(self.ontology)
def add_header(name):
"""Help function to add header row to table."""
clsname = f"element-table-{name.lower().replace(' ', '-')}"
lines.extend(
[
" <tr>",
f' <th class="{clsname}" colspan="2">{name}</th>',
" </tr>",
]
)
def _get_links(item, key):
"""Get HTML links for a list of entitities that
can be fetched from the ontology as keys."""
links = []
for ent in item[key]:
full_iri = ent.iri
try:
val = ent.prefLabel.get_lang("en")[0]
except (IndexError, AttributeError):
val = ent
links.append(_html_links(full_iri, display_text=val))
return links
def _linkify_manchester(text: str, onto) -> str:
"""
Convert manchester notation as string to HTML links.
"""
def _replace(match):
word = match.group(0)
try:
full = onto[word].iri
return _html_links(full, word)
except (KeyError, AttributeError):
return word
return re.sub(r"\w+", _replace, text)
def _html_links(full_iri, display_text):
"""Create the HTML code so that links lead to
the correct fragment in the same document if possibe,
otherwise link to the full IRI"""
fragment_iri = full_iri.split("#")[-1]
return (
f"<a href='#{fragment_iri}' "
f'onclick="'
f"if(!document.getElementById('{fragment_iri}'))"
f"{{window.location.href='{full_iri}'; return false;}}"
f'">'
f"{display_text}</a>"
)
def _display_iri_label(iri: str) -> str:
"""Return a compact display label for well-known IRIs."""
if iri.startswith("http://www.w3.org/2001/XMLSchema#"):
return f"xsd:{iri.split('#', maxsplit=1)[-1]}"
return iri
def _linkify_value(val: str) -> str:
"""
If `val` contains one or more IRIs, return them as separate links.
- If exactly one IRI and it's an image, embed the image.
- Otherwise, link each IRI separately and join with '; '.
"""
if not isinstance(val, str):
return val
# find IRIs (separated by ; , or whitespace)
urls = re.findall(r"https?://[^\s;,]+", val)
if not urls:
return val
# single image → embed
if len(urls) == 1 and urls[0].lower().endswith(
(".png", ".jpg", ".jpeg", ".gif", ".svg")
):
u = urls[0]
return (
f'<a href="{u}"><img src="{u}" alt="{u}" '
f'style="max-width:400px; max-height:300px;"/></a>'
)
# otherwise, link each separately with a compact display label
links = []
for u in urls:
links.append(_html_links(u, _display_iri_label(u)))
return "; ".join(links)
def add_keyvalue(
key,
value,
iri=None,
):
"""Help function for adding a key-value row to table.
Arguments:
key: Key to show in the table.
value: Value to show in the table.
iri: IRI to link to, if value does not have attribute .iri.
"""
if not isinstance(value, list):
values = [value]
else:
values = value
strval = ""
count = 0
for val in values:
if count > 0 and not key == "Restrictions":
strval += ", "
count += 1
if hasattr(val, "iri"):
strval += _html_links(val.iri, get_label(val))
elif iri:
strval += _html_links(iri, val)
elif key == "Restrictions":
strval += (
"<li>"
+ _linkify_manchester(
asstring(val),
self.ontology,
)
+ "</li>"
)
# if value is a class 'type'
else:
strval += _linkify_value(val)
strval = strval.replace("\n", "<br>")
# Build a self-contained snippet to prevent table misalignment
if key == "Restrictions":
strval = (
f'<div class="restriction-list"><ul>{strval}</ul></div>'
)
lines.extend(
[
" <tr>",
' <td class="element-table-key">'
f'<span class="element-table-key">'
f"{key}</span></td>",
f' <td class="element-table-value">{strval}</td>',
" </tr>",
]
)
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)
navid = navid2 = ""
if entity.name in self.navids:
warnings.warn(f"duplicated entity names: {entity.name}")
else:
self.navids.add(entity.name)
navid = f' <div id="{entity.name}"></div>'
if hasattr(entity, "prefLabel"):
preflabel = str(entity.prefLabel.first())
if preflabel != entity.name:
if preflabel in self.navids:
warnings.warn(f"duplicated prefLabel: {preflabel}")
else:
self.navids.add(preflabel)
navid2 = f' <div id="{preflabel}"></div>'
lines.extend(
[
".. raw:: html",
"",
navid,
navid2,
"",
f"{label}",
"^" * len(label),
"",
".. raw:: html",
"",
' <table class="element-table">',
]
)
add_keyvalue("IRI", entity.iri, entity.iri)
if hasattr(entity, "get_annotations") or hasattr(
entity, "get_individual_annotations"
):
add_header("Annotations")
annotations = { # pylint: disable=protected-access
a: a._get_values_for_class( # pylint: disable=protected-access
entity
)
for a in annotations_ranked
if a._get_values_for_class( # pylint: disable=protected-access
entity
)
}
long_annotations = [
"http://www.w3.org/2004/02/skos/core#example",
"https://w3id.org/emmo#"
"EMMO_c7b62dd7_063a_4c2a_8504_42f7264ba83f",
"https://w3id.org/emmo#EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9",
"https://w3id.org/emmo#EMMO_31252f35_c767_4b97_a877_1235076c3e13",
"https://w3id.org/emmo#EMMO_70fe84ff_99b6_4206_a9fc_9a8931836d84",
]
table_annotations = {
key: value
for key, value in annotations.items()
if get_label(key) not in CALLOUTS
}
for key, item in table_annotations.items():
if key.iri not in long_annotations:
add_keyvalue(get_label(key), table_annotations[key])
else:
add_keyvalue(get_label(key), item)
# Fetch parents (all direct superclasses)
parents = [
ent
for ent in entity.is_a
if (
isinstance(
ent,
(owlready2.ThingClass, owlready2.PropertyClass),
)
)
]
# Fetch direct subclasses
subclasses = (
list(entity.subclasses())
if isinstance(entity, owlready2.ThingClass)
else []
)
# Fetch OWL restrictions (object property + someValuesFrom)
restrictions = [
restriction
for restriction in entity.is_a
if isinstance(restriction, owlready2.Restriction)
]
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='<a href="{iri}">{label}</a>',
ontology=self.ontology,
),
)
# Add SubclassOf/SubPropertyOf/InstanceOf for direct parents
if isinstance(entity, owlready2.ThingClass):
add_keyvalue("Subclass Of", parents)
elif isinstance(entity, (owlready2.PropertyClass)):
add_keyvalue("Subproperty Of", parents)
elif isinstance(entity, owlready2.Thing):
add_keyvalue("Instance of", parents)
# Add Subclasses if any
if subclasses:
add_keyvalue("Subclasses", subclasses)
# Add Restrictions if any
if restrictions:
add_keyvalue("Restrictions", restrictions)
if isinstance(entity, owlready2.PropertyClass):
# Add domain and range for properties
try:
# Remove None from domain list if present (Owlready2 quirk)
entity.domain = [
d for d in entity.domain if d is not None
]
if entity.domain:
add_keyvalue("Domain", entity.domain)
except (NoSuchLabelError, AttributeError):
pass
try:
# Remove None from range lists if present
# (Owlready2 quirk). Use the range IRI only for
# Python datatypes, otherwise keep the ontology
# entity so it is rendered as an internal link.
ranges = [
r for r in entity.range if r is not None
]
range_iris = [
r for r in entity.range_iri if r is not None
]
range_values = [
(
range_
if hasattr(range_, "iri")
else range_iri
)
for range_, range_iri in zip(
ranges, range_iris
)
]
if range_values:
add_keyvalue("Range", range_values)
except (NoSuchLabelError, AttributeError):
pass
lines.extend([" </table>", ""])
# raw html block content (indented)
lines.extend(
[
" </table>",
"", # end of indented raw content
"", # blank line after raw directive block
]
)
callout_annotations = {
key: value
for key, value in annotations.items()
if get_label(key) in CALLOUTS
}
def _indent(block: str, n: int = 3) -> str:
pad = " " * n
return "\n".join(
(pad + ln) if ln.strip() else ""
for ln in block.splitlines()
)
for key, item in callout_annotations.items():
directive, title = CALLOUTS[get_label(key)]
lines.extend(
[
f".. {directive}::"
+ (f" {title}" if title else "")
+ "\n\n"
]
)
content = _extract_all_annotations(item)
content = "\n\n".join(content)
content = _indent(content, n=3)
lines.extend([content, ""])
lines.extend(["\n"]) # blank line between callouts
lines = "\n".join(lines)
return lines
add_entity(self, entity)
¶
Add entity (class, property, individual, datatype) to list of
entities to document.
Source code in ontopy/ontodoc_rst.py
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.data_properties.add(entity)
elif isinstance(entity, owlready2.AnnotationPropertyClass):
self.annotation_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)
add_ontology(self, ontology, imported=False)
¶
Add ontology to documentation.
Source code in ontopy/ontodoc_rst.py
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)
get_header(self)
¶
Return a the reStructuredText header as a string.
Source code in ontopy/ontodoc_rst.py
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)}
"""
get_refdoc(self, subsections='all', header=True)
¶
Return reference documentation of all module entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
subsections |
str |
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. |
'all' |
header |
bool |
Whether to also include the header in the returned documentation. |
True |
Returns:
| Type | Description |
|---|---|
str |
String with reference documentation. |
Source code in ontopy/ontodoc_rst.py
def get_refdoc(
self,
subsections: str = "all",
header: bool = True,
) -> str:
# pylint: disable=too-many-branches,too-many-locals,too-many-statements
"""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())
annotations_ranked = _get_annotation_rank(self.ontology)
def add_header(name):
"""Help function to add header row to table."""
clsname = f"element-table-{name.lower().replace(' ', '-')}"
lines.extend(
[
" <tr>",
f' <th class="{clsname}" colspan="2">{name}</th>',
" </tr>",
]
)
def _get_links(item, key):
"""Get HTML links for a list of entitities that
can be fetched from the ontology as keys."""
links = []
for ent in item[key]:
full_iri = ent.iri
try:
val = ent.prefLabel.get_lang("en")[0]
except (IndexError, AttributeError):
val = ent
links.append(_html_links(full_iri, display_text=val))
return links
def _linkify_manchester(text: str, onto) -> str:
"""
Convert manchester notation as string to HTML links.
"""
def _replace(match):
word = match.group(0)
try:
full = onto[word].iri
return _html_links(full, word)
except (KeyError, AttributeError):
return word
return re.sub(r"\w+", _replace, text)
def _html_links(full_iri, display_text):
"""Create the HTML code so that links lead to
the correct fragment in the same document if possibe,
otherwise link to the full IRI"""
fragment_iri = full_iri.split("#")[-1]
return (
f"<a href='#{fragment_iri}' "
f'onclick="'
f"if(!document.getElementById('{fragment_iri}'))"
f"{{window.location.href='{full_iri}'; return false;}}"
f'">'
f"{display_text}</a>"
)
def _display_iri_label(iri: str) -> str:
"""Return a compact display label for well-known IRIs."""
if iri.startswith("http://www.w3.org/2001/XMLSchema#"):
return f"xsd:{iri.split('#', maxsplit=1)[-1]}"
return iri
def _linkify_value(val: str) -> str:
"""
If `val` contains one or more IRIs, return them as separate links.
- If exactly one IRI and it's an image, embed the image.
- Otherwise, link each IRI separately and join with '; '.
"""
if not isinstance(val, str):
return val
# find IRIs (separated by ; , or whitespace)
urls = re.findall(r"https?://[^\s;,]+", val)
if not urls:
return val
# single image → embed
if len(urls) == 1 and urls[0].lower().endswith(
(".png", ".jpg", ".jpeg", ".gif", ".svg")
):
u = urls[0]
return (
f'<a href="{u}"><img src="{u}" alt="{u}" '
f'style="max-width:400px; max-height:300px;"/></a>'
)
# otherwise, link each separately with a compact display label
links = []
for u in urls:
links.append(_html_links(u, _display_iri_label(u)))
return "; ".join(links)
def add_keyvalue(
key,
value,
iri=None,
):
"""Help function for adding a key-value row to table.
Arguments:
key: Key to show in the table.
value: Value to show in the table.
iri: IRI to link to, if value does not have attribute .iri.
"""
if not isinstance(value, list):
values = [value]
else:
values = value
strval = ""
count = 0
for val in values:
if count > 0 and not key == "Restrictions":
strval += ", "
count += 1
if hasattr(val, "iri"):
strval += _html_links(val.iri, get_label(val))
elif iri:
strval += _html_links(iri, val)
elif key == "Restrictions":
strval += (
"<li>"
+ _linkify_manchester(
asstring(val),
self.ontology,
)
+ "</li>"
)
# if value is a class 'type'
else:
strval += _linkify_value(val)
strval = strval.replace("\n", "<br>")
# Build a self-contained snippet to prevent table misalignment
if key == "Restrictions":
strval = (
f'<div class="restriction-list"><ul>{strval}</ul></div>'
)
lines.extend(
[
" <tr>",
' <td class="element-table-key">'
f'<span class="element-table-key">'
f"{key}</span></td>",
f' <td class="element-table-value">{strval}</td>',
" </tr>",
]
)
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)
navid = navid2 = ""
if entity.name in self.navids:
warnings.warn(f"duplicated entity names: {entity.name}")
else:
self.navids.add(entity.name)
navid = f' <div id="{entity.name}"></div>'
if hasattr(entity, "prefLabel"):
preflabel = str(entity.prefLabel.first())
if preflabel != entity.name:
if preflabel in self.navids:
warnings.warn(f"duplicated prefLabel: {preflabel}")
else:
self.navids.add(preflabel)
navid2 = f' <div id="{preflabel}"></div>'
lines.extend(
[
".. raw:: html",
"",
navid,
navid2,
"",
f"{label}",
"^" * len(label),
"",
".. raw:: html",
"",
' <table class="element-table">',
]
)
add_keyvalue("IRI", entity.iri, entity.iri)
if hasattr(entity, "get_annotations") or hasattr(
entity, "get_individual_annotations"
):
add_header("Annotations")
annotations = { # pylint: disable=protected-access
a: a._get_values_for_class( # pylint: disable=protected-access
entity
)
for a in annotations_ranked
if a._get_values_for_class( # pylint: disable=protected-access
entity
)
}
long_annotations = [
"http://www.w3.org/2004/02/skos/core#example",
"https://w3id.org/emmo#"
"EMMO_c7b62dd7_063a_4c2a_8504_42f7264ba83f",
"https://w3id.org/emmo#EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9",
"https://w3id.org/emmo#EMMO_31252f35_c767_4b97_a877_1235076c3e13",
"https://w3id.org/emmo#EMMO_70fe84ff_99b6_4206_a9fc_9a8931836d84",
]
table_annotations = {
key: value
for key, value in annotations.items()
if get_label(key) not in CALLOUTS
}
for key, item in table_annotations.items():
if key.iri not in long_annotations:
add_keyvalue(get_label(key), table_annotations[key])
else:
add_keyvalue(get_label(key), item)
# Fetch parents (all direct superclasses)
parents = [
ent
for ent in entity.is_a
if (
isinstance(
ent,
(owlready2.ThingClass, owlready2.PropertyClass),
)
)
]
# Fetch direct subclasses
subclasses = (
list(entity.subclasses())
if isinstance(entity, owlready2.ThingClass)
else []
)
# Fetch OWL restrictions (object property + someValuesFrom)
restrictions = [
restriction
for restriction in entity.is_a
if isinstance(restriction, owlready2.Restriction)
]
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='<a href="{iri}">{label}</a>',
ontology=self.ontology,
),
)
# Add SubclassOf/SubPropertyOf/InstanceOf for direct parents
if isinstance(entity, owlready2.ThingClass):
add_keyvalue("Subclass Of", parents)
elif isinstance(entity, (owlready2.PropertyClass)):
add_keyvalue("Subproperty Of", parents)
elif isinstance(entity, owlready2.Thing):
add_keyvalue("Instance of", parents)
# Add Subclasses if any
if subclasses:
add_keyvalue("Subclasses", subclasses)
# Add Restrictions if any
if restrictions:
add_keyvalue("Restrictions", restrictions)
if isinstance(entity, owlready2.PropertyClass):
# Add domain and range for properties
try:
# Remove None from domain list if present (Owlready2 quirk)
entity.domain = [
d for d in entity.domain if d is not None
]
if entity.domain:
add_keyvalue("Domain", entity.domain)
except (NoSuchLabelError, AttributeError):
pass
try:
# Remove None from range lists if present
# (Owlready2 quirk). Use the range IRI only for
# Python datatypes, otherwise keep the ontology
# entity so it is rendered as an internal link.
ranges = [
r for r in entity.range if r is not None
]
range_iris = [
r for r in entity.range_iri if r is not None
]
range_values = [
(
range_
if hasattr(range_, "iri")
else range_iri
)
for range_, range_iri in zip(
ranges, range_iris
)
]
if range_values:
add_keyvalue("Range", range_values)
except (NoSuchLabelError, AttributeError):
pass
lines.extend([" </table>", ""])
# raw html block content (indented)
lines.extend(
[
" </table>",
"", # end of indented raw content
"", # blank line after raw directive block
]
)
callout_annotations = {
key: value
for key, value in annotations.items()
if get_label(key) in CALLOUTS
}
def _indent(block: str, n: int = 3) -> str:
pad = " " * n
return "\n".join(
(pad + ln) if ln.strip() else ""
for ln in block.splitlines()
)
for key, item in callout_annotations.items():
directive, title = CALLOUTS[get_label(key)]
lines.extend(
[
f".. {directive}::"
+ (f" {title}" if title else "")
+ "\n\n"
]
)
content = _extract_all_annotations(item)
content = "\n\n".join(content)
content = _indent(content, n=3)
lines.extend([content, ""])
lines.extend(["\n"]) # blank line between callouts
lines = "\n".join(lines)
return lines
get_title(self)
¶
Return a module title.
Source code in ontopy/ontodoc_rst.py
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
nonempty(self)
¶
Returns whether the module has any classes, properties, individuals or datatypes.
Source code in ontopy/ontodoc_rst.py
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
)
OntologyDocumentation
¶
Documentation for an ontology with a common namespace.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
ontologies |
Iterable[Ontology] |
Ontologies to include in the generated documentation. All entities in these ontologies will be included. |
required |
imported |
bool |
Whether to include imported ontologies. |
True |
recursive |
bool |
Whether to recursively import all imported ontologies.
Implies |
False |
iri_regex |
Optional[str] |
A regular expression that the IRI of documented entities should match. |
None |
Source code in ontopy/ontodoc_rst.py
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}.rst>
.. include:: ../README.md
:parser: myst_parser.sphinx_
"""
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( ## pylint: disable=too-many-locals,too-many-statements
self,
conffile="conf.py",
docfile=None,
overwrite=False,
github_repository=None,
):
"""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.
github_repository: Optional GitHub repository in the form
"OWNER/REPO".
"""
# 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")
# FIXME: If authors are URIs, extract their names from the URI
author = (
", ".join(
a.value if hasattr(a, "value") else str(a) for a in authors
)
if authors
else "<AUTHOR>"
)
copyright = license if license else f"{time.strftime('%Y')}, {author}"
if github_repository and "/" in github_repository:
owner, repo = github_repository.split("/", 1)
github_url = f"https://github.com/{owner}/{repo}"
widoco_url = (
f"https://{owner}.github.io/{repo}/widoco/index-en.html"
)
else:
github_url = (
"https://github.com/"
f"emmo-repo/domain-{md.ontology.name.lower()}"
)
widoco_url = (
"https://emmo-repo.github.io/"
f"domain-{md.ontology.name.lower()}/widoco/index-en.html"
)
# pylint: disable=line-too-long
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 = [
"myst_parser",
]
source_suffix = {{
".rst": "restructuredtext",
".md": "markdown",
}}
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# Pygments styles are Sphinx settings (not theme options)
pygments_style = "friendly"
pygments_dark_style = "lightbulb"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "pydata_sphinx_theme"
html_theme_options = {{
# Remove left sidebar content (primary sidebar)
"primary_sidebar_items": [],
# Navigation depth (only matters if you show a nav in the sidebar)
"show_nav_level": 2,
# Disable right "On this page" TOC everywhere
"show_toc_level": 0,
"secondary_sidebar_items": [],
# Navbar
"navbar_center": ["navbar-nav"],
"navbar_end": ["navbar-icon-links", "theme-switcher", "search-button"],
# Icon links (Font Awesome 6 classes)
"icon_links": [
{{
"name": "GitHub",
"url": "{github_url}",
"icon": "fa-brands fa-github",
}},
{{
"name": "Ontology Homepage",
"url": "{iri}",
"icon": "fa-solid fa-globe",
}},
{{
"name": "WIDOCO Documentation",
"url": "{widoco_url}",
"icon": "fa-solid fa-file-lines",
}}
],
"show_prev_next": False,
"footer_start": ["copyright"],
"footer_center": ["sphinx-version"],
}}
html_static_path = ["_static"]
html_title = f"{md.ontology.name.capitalize()} Ontology"
html_css_files = ["custom.css"]
html_js_files = ["toc-collapsible.js"]
# html_sidebars keys are docname globs. Apply everywhere unless you truly want per-page overrides.
html_sidebars = {{
"{md.ontology.name.lower()}": ["search-field.html", "page-toc.html", "edit-this-page.html"],
}}
"""
if not conffile:
conffile = Path(docfile).with_name("conf.py")
if not overwrite and conffile.exists():
warnings.warn(f"conf.py file already exists: {conffile}")
return
conffile.write_text(content, encoding="utf8")
def copy_css_file(
self,
source: str | Path = SETUPTEMPLATES_DIR / "css" / "custom.css",
) -> Path:
"""
Copy a custom CSS file into the Sphinx HTML static directory.
The source may be:
- a URL (http/https),
- an absolute local path,
- a relative local path.
Parameters
----------
source : str or pathlib.Path, optional
Location of the CSS file to copy.
Returns
-------
pathlib.Path
Path to the copied CSS file.
"""
static_dir = Path("build") / "_static"
static_dir.mkdir(parents=True, exist_ok=True)
destination = static_dir / "custom.css"
# URL source
if urlparse(str(source)).scheme in ("http", "https"):
with urlopen(source) as response, open( # nosec
destination, "wb"
) as f: # nosec
shutil.copyfileobj(response, f)
# Local path source
else:
source_path = Path(source)
if not source_path.exists():
raise FileNotFoundError(f"CSS source not found: {source_path}")
shutil.copyfile(source_path, destination)
print(f"Copied CSS file to: {destination}")
return destination
def copy_js_file(
self,
source: str | Path = (SETUPTEMPLATES_DIR / "js" / "toc-collapsible.js"),
) -> Path:
"""
Copy the collapsible-TOC JavaScript file into the Sphinx HTML
static directory.
The source may be:
- a URL (http/https),
- an absolute local path,
- a relative local path.
Parameters
----------
source : str or pathlib.Path, optional
Location of the JS file to copy.
Returns
-------
pathlib.Path
Path to the copied JS file.
"""
static_dir = Path("build") / "_static"
static_dir.mkdir(parents=True, exist_ok=True)
destination = static_dir / "toc-collapsible.js"
# URL source
if urlparse(str(source)).scheme in ("http", "https"):
with urlopen(source) as response, open( # nosec
destination, "wb"
) as f: # nosec
shutil.copyfileobj(response, f)
# Local path source
else:
source_path = Path(source)
if not source_path.exists():
raise FileNotFoundError(f"JS source not found: {source_path}")
shutil.copyfile(source_path, destination)
print(f"Copied JS file to: {destination}")
return destination
copy_css_file(self, source=PosixPath('/home/runner/work/EMMOntoPy/EMMOntoPy/ontopy/ontokit/setuptemplates/css/custom.css'))
¶
Copy a custom CSS file into the Sphinx HTML static directory.
The source may be: - a URL (http/https), - an absolute local path, - a relative local path.
Parameters¶
source : str or pathlib.Path, optional Location of the CSS file to copy.
Returns¶
pathlib.Path Path to the copied CSS file.
Source code in ontopy/ontodoc_rst.py
def copy_css_file(
self,
source: str | Path = SETUPTEMPLATES_DIR / "css" / "custom.css",
) -> Path:
"""
Copy a custom CSS file into the Sphinx HTML static directory.
The source may be:
- a URL (http/https),
- an absolute local path,
- a relative local path.
Parameters
----------
source : str or pathlib.Path, optional
Location of the CSS file to copy.
Returns
-------
pathlib.Path
Path to the copied CSS file.
"""
static_dir = Path("build") / "_static"
static_dir.mkdir(parents=True, exist_ok=True)
destination = static_dir / "custom.css"
# URL source
if urlparse(str(source)).scheme in ("http", "https"):
with urlopen(source) as response, open( # nosec
destination, "wb"
) as f: # nosec
shutil.copyfileobj(response, f)
# Local path source
else:
source_path = Path(source)
if not source_path.exists():
raise FileNotFoundError(f"CSS source not found: {source_path}")
shutil.copyfile(source_path, destination)
print(f"Copied CSS file to: {destination}")
return destination
copy_js_file(self, source=PosixPath('/home/runner/work/EMMOntoPy/EMMOntoPy/ontopy/ontokit/setuptemplates/js/toc-collapsible.js'))
¶
Copy the collapsible-TOC JavaScript file into the Sphinx HTML static directory.
The source may be: - a URL (http/https), - an absolute local path, - a relative local path.
Parameters¶
source : str or pathlib.Path, optional Location of the JS file to copy.
Returns¶
pathlib.Path Path to the copied JS file.
Source code in ontopy/ontodoc_rst.py
def copy_js_file(
self,
source: str | Path = (SETUPTEMPLATES_DIR / "js" / "toc-collapsible.js"),
) -> Path:
"""
Copy the collapsible-TOC JavaScript file into the Sphinx HTML
static directory.
The source may be:
- a URL (http/https),
- an absolute local path,
- a relative local path.
Parameters
----------
source : str or pathlib.Path, optional
Location of the JS file to copy.
Returns
-------
pathlib.Path
Path to the copied JS file.
"""
static_dir = Path("build") / "_static"
static_dir.mkdir(parents=True, exist_ok=True)
destination = static_dir / "toc-collapsible.js"
# URL source
if urlparse(str(source)).scheme in ("http", "https"):
with urlopen(source) as response, open( # nosec
destination, "wb"
) as f: # nosec
shutil.copyfileobj(response, f)
# Local path source
else:
source_path = Path(source)
if not source_path.exists():
raise FileNotFoundError(f"JS source not found: {source_path}")
shutil.copyfile(source_path, destination)
print(f"Copied JS file to: {destination}")
return destination
get_header(self)
¶
Return a the reStructuredText header as a string.
Source code in ontopy/ontodoc_rst.py
def get_header(self) -> str:
"""Return a the reStructuredText header as a string."""
return """
==========
References
==========
"""
get_refdoc(self, header=True, subsections='all')
¶
Return reference documentation of all module entities.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
header |
bool |
Whether to also include the header in the returned documentation. |
True |
subsections |
str |
Comma-separated list of subsections to include in the returned documentation. See ModuleDocumentation.get_refdoc() for more info. |
'all' |
Returns:
| Type | Description |
|---|---|
str |
String with reference documentation. |
Source code in ontopy/ontodoc_rst.py
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)
top_ontology(self)
¶
Return the top-level ontology.
Source code in ontopy/ontodoc_rst.py
def top_ontology(self) -> Ontology:
"""Return the top-level ontology."""
return self.module_documentations[0].ontology
write_conf_template(self, conffile='conf.py', docfile=None, overwrite=False, github_repository=None)
¶
Write basic template sphinx conf.py file to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
conffile |
Name of configuration file to write. |
'conf.py' |
|
docfile |
Name of generated documentation file. If not given, the name of the top ontology will be used. |
None |
|
overwrite |
Whether to overwrite an existing file. |
False |
|
github_repository |
Optional GitHub repository in the form "OWNER/REPO". |
None |
Source code in ontopy/ontodoc_rst.py
def write_conf_template( ## pylint: disable=too-many-locals,too-many-statements
self,
conffile="conf.py",
docfile=None,
overwrite=False,
github_repository=None,
):
"""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.
github_repository: Optional GitHub repository in the form
"OWNER/REPO".
"""
# 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")
# FIXME: If authors are URIs, extract their names from the URI
author = (
", ".join(
a.value if hasattr(a, "value") else str(a) for a in authors
)
if authors
else "<AUTHOR>"
)
copyright = license if license else f"{time.strftime('%Y')}, {author}"
if github_repository and "/" in github_repository:
owner, repo = github_repository.split("/", 1)
github_url = f"https://github.com/{owner}/{repo}"
widoco_url = (
f"https://{owner}.github.io/{repo}/widoco/index-en.html"
)
else:
github_url = (
"https://github.com/"
f"emmo-repo/domain-{md.ontology.name.lower()}"
)
widoco_url = (
"https://emmo-repo.github.io/"
f"domain-{md.ontology.name.lower()}/widoco/index-en.html"
)
# pylint: disable=line-too-long
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 = [
"myst_parser",
]
source_suffix = {{
".rst": "restructuredtext",
".md": "markdown",
}}
templates_path = ['_templates']
exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
# Pygments styles are Sphinx settings (not theme options)
pygments_style = "friendly"
pygments_dark_style = "lightbulb"
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = "pydata_sphinx_theme"
html_theme_options = {{
# Remove left sidebar content (primary sidebar)
"primary_sidebar_items": [],
# Navigation depth (only matters if you show a nav in the sidebar)
"show_nav_level": 2,
# Disable right "On this page" TOC everywhere
"show_toc_level": 0,
"secondary_sidebar_items": [],
# Navbar
"navbar_center": ["navbar-nav"],
"navbar_end": ["navbar-icon-links", "theme-switcher", "search-button"],
# Icon links (Font Awesome 6 classes)
"icon_links": [
{{
"name": "GitHub",
"url": "{github_url}",
"icon": "fa-brands fa-github",
}},
{{
"name": "Ontology Homepage",
"url": "{iri}",
"icon": "fa-solid fa-globe",
}},
{{
"name": "WIDOCO Documentation",
"url": "{widoco_url}",
"icon": "fa-solid fa-file-lines",
}}
],
"show_prev_next": False,
"footer_start": ["copyright"],
"footer_center": ["sphinx-version"],
}}
html_static_path = ["_static"]
html_title = f"{md.ontology.name.capitalize()} Ontology"
html_css_files = ["custom.css"]
html_js_files = ["toc-collapsible.js"]
# html_sidebars keys are docname globs. Apply everywhere unless you truly want per-page overrides.
html_sidebars = {{
"{md.ontology.name.lower()}": ["search-field.html", "page-toc.html", "edit-this-page.html"],
}}
"""
if not conffile:
conffile = Path(docfile).with_name("conf.py")
if not overwrite and conffile.exists():
warnings.warn(f"conf.py file already exists: {conffile}")
return
conffile.write_text(content, encoding="utf8")
write_index_template(self, indexfile='index.rst', docfile=None, overwrite=False)
¶
Write a basic template index.rst file to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
indexfile |
Name of index file to write. |
'index.rst' |
|
docfile |
Name of generated documentation file. If not given, the name of the top ontology will be used. |
None |
|
overwrite |
Whether to overwrite an existing file. |
False |
Source code in ontopy/ontodoc_rst.py
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}.rst>
.. include:: ../README.md
:parser: myst_parser.sphinx_
"""
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")
write_refdoc(self, docfile=None, subsections='all')
¶
Write reference documentation to disk.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
docfile |
Name of file to write to. Defaults to the name of
the top ontology with extension |
None |
|
subsections |
Comma-separated list of subsections to include in the returned documentation. See ModuleDocumentation.get_refdoc() for more info. |
'all' |
Source code in ontopy/ontodoc_rst.py
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"
)