Skip to content

Commit

Permalink
Add functionality for setting name part of IRI to prefLabel (#399)
Browse files Browse the repository at this point in the history
Added --rename-iris and --annotate-source options of ontoconvert.
In order to allow combining these options with with the --squash, --recursive and --reasoner options, the --squash and --recursive functionality had to be moved into Ontology.save().

Changes:
implemented ontopy.utils.rename_iris()
implemented ontopy.utils.annotate_sources()
improved writing of catalog files
added recursive, squash and write_catalog_file keyword arguments to Ontology.save()
cleaned up Ontology.sync_reasoner()
added new options to the ontoconvert tool
added a workaround to ontoconvert for correct reasoning using rdflib - if the related bug in Owlready2 is not fixed soon, we should consider to add a correct implementation of owlready2.reasoning._apply_reasoning_results() to EMMOntoPy

* Fixed the rename_iris() and annotate_source() utility functions.

* Completed implementation of rename_iris() - it dragged a lot of changes with it..

* Added tests to new functions in utils.

* Added interactive test environment

* Uppdated annotate_source to use rdfs:isDefinedBy and fixed tests
  • Loading branch information
jesper-friis authored Apr 27, 2022
1 parent b7f4934 commit 5cdb24e
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 207 deletions.
5 changes: 5 additions & 0 deletions docs/tools-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,8 @@ Since the catalog file will be overwritten in the above example writing output t
```console
ontoconvert --recursive emmo.ttl owl/emmo.owl
```


### Bugs
Since parsing the results from the reasoner is currently broken in Owlready2 (v0.37), a workaround has been added to ontoconvert.
This workaround only only supports FaCT++. Hence, HermiT and Pellet are currently not available.
17 changes: 12 additions & 5 deletions ontopy/factpluspluswrapper/sync_factpp.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@
RDF.type: "individual",
OWL.equivalentClass: "class",
OWL.equivalentProperty: "property",
OWL.sameAs: "individual",
}


def sync_reasoner_factpp( # pylint: disable=too-many-locals,too-many-branches
def sync_reasoner_factpp(
ontology_or_world=None, infer_property_values=False, debug=1
):
"""Run FaCT++ reasoner and load the inferred relations back into
Expand All @@ -41,6 +42,7 @@ def sync_reasoner_factpp( # pylint: disable=too-many-locals,too-many-branches
debug : bool
Whether to print debug info to standard output.
"""
# pylint: disable=too-many-locals,too-many-branches,too-many-statements
if isinstance(ontology_or_world, World):
world = ontology_or_world
elif isinstance(ontology_or_world, Ontology):
Expand All @@ -62,7 +64,8 @@ def sync_reasoner_factpp( # pylint: disable=too-many-locals,too-many-branches
world.graph.release_write_lock() # Not needed during reasoning

try:
print("*** Prepare graph")
if debug:
print("*** Prepare graph")
# Exclude owl:imports because they are not needed and can
# cause trouble when loading the inferred ontology
graph1 = rdflib.Graph()
Expand All @@ -72,10 +75,12 @@ def sync_reasoner_factpp( # pylint: disable=too-many-locals,too-many-branches
if predicate != OWL.imports:
graph1.add((subject, predicate, obj))

print("*** Run FaCT++ reasoner (and postprocess)")
if debug:
print("*** Run FaCT++ reasoner (and postprocess)")
graph2 = FaCTPPGraph(graph1).inferred_graph()

print("*** Load inferred ontology")
if debug:
print("*** Load inferred ontology")
# Check all rdfs:subClassOf relations in the inferred graph and add
# them to the world if they are missing
new_parents = defaultdict(list)
Expand Down Expand Up @@ -117,7 +122,9 @@ def sync_reasoner_factpp( # pylint: disable=too-many-locals,too-many-branches
if locked:
world.graph.acquire_write_lock() # re-lock when applying results

print("*** Applying reasoning results")
if debug:
print("*** Applying reasoning results")

_apply_reasoning_results(
world, ontology, debug, new_parents, new_equivs, entity_2_type
)
Expand Down
199 changes: 160 additions & 39 deletions ontopy/ontology.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@
import uuid
import tempfile
import types
import pathlib
from typing import Union
from pathlib import Path
from collections import defaultdict
from collections.abc import Iterable

Expand All @@ -32,6 +32,7 @@
from ontopy.utils import (
asstring,
read_catalog,
write_catalog,
infer_version,
convert_imported,
FMAP,
Expand Down Expand Up @@ -84,11 +85,7 @@ def get_ontology(self, base_iri="emmo-inferred"):
- "emmo-development": load latest inferred development version
of EMMO
"""
base_iri = (
base_iri.as_uri()
if isinstance(base_iri, pathlib.Path)
else base_iri
)
base_iri = base_iri.as_uri() if isinstance(base_iri, Path) else base_iri

if base_iri == "emmo":
base_iri = (
Expand Down Expand Up @@ -305,10 +302,10 @@ def get_by_label(
return self.namespaces[label]

if label_annotations is None:
annotations = (_.name for _ in self.label_annotations)
annotations = (a.name for a in self.label_annotations)
else:
annotations = (
_.name if hasattr(_, "storid") else _ for _ in label_annotations
a.name if hasattr(a, "storid") else a for a in label_annotations
)
for key in annotations:
entity = self.search_one(**{key: label})
Expand Down Expand Up @@ -374,7 +371,7 @@ def remove_label_annotation(self, iri):
raise ValueError(f"IRI not in ontology: {iri}")
self._label_annotations.remove(label_annotation)

def load( # pylint: disable=too-many-arguments
def load( # pylint: disable=too-many-arguments,arguments-renamed
self,
only_local=False,
filename=None,
Expand Down Expand Up @@ -460,7 +457,7 @@ def _load( # pylint: disable=too-many-arguments,too-many-locals,too-many-branch
catalog_file="catalog-v001.xml",
**kwargs,
):
"""Help function for _load()."""
"""Help function for load()."""
web_protocol = "http://", "https://", "ftp://"

url = filename if filename else self.base_iri.rstrip("/#")
Expand Down Expand Up @@ -612,19 +609,52 @@ def getmtime(path):
)

def save(
self, filename=None, format=None, overwrite=False, **kwargs
): # pylint: disable=redefined-builtin
self,
filename=None,
format=None,
dir=".",
mkdir=False,
overwrite=False,
recursive=False,
squash=False,
write_catalog_file=False,
append_catalog=False,
catalog_file="catalog-v001.xml",
):
"""Writes the ontology to file.
If `overwrite` is `True` and filename exists, it will be removed
before saving. The default is to append an existing ontology.
Parameters
----------
filename: None | str | Path
Name of file to write to. If None, it defaults to the name
of the ontology with `format` as file extension.
format: str
Output format. The default is to infer it from `filename`.
dir: str | Path
If `filename` is a relative path, it is a relative path to `dir`.
mkdir: bool
Whether to create output directory if it does not exists.
owerwrite: bool
If true and `filename` exists, remove the existing file before
saving. The default is to append to an existing ontology.
recursive: bool
Whether to save imported ontologies recursively. This is
commonly combined with `filename=None`, `dir` and `mkdir`.
squash: bool
If true, rdflib will be used to save the current ontology
together with all its sub-ontologies into `filename`.
It make no sense to combine this with `recursive`.
write_catalog_file: bool
Whether to also write a catalog file to disk.
append_catalog: bool
Whether to append to an existing catalog file.
catalog_file: str | Path
Name of catalog file. If not an absolute path, it is prepended
to `dir`.
"""
if overwrite and filename and os.path.exists(filename):
os.remove(filename)

if not format:
format = guess_format(filename, fmap=FMAP)

# pylint: disable=redefined-builtin,too-many-arguments
# pylint: disable=too-many-statements,too-many-branches
# pylint: disable=too-many-locals,arguments-renamed
if not _validate_installed_version(
package="rdflib", min_version="6.0.0"
) and format == FMAP.get("ttl", ""):
Expand All @@ -642,16 +672,104 @@ def save(
)
)

if format in OWLREADY2_FORMATS:
revmap = {value: key for key, value in FMAP.items()}
super().save(file=filename, format=revmap[format], **kwargs)
revmap = {value: key for key, value in FMAP.items()}

if filename is None:
if format:
fmt = revmap.get(format, format)
filename = f"{self.name}.{fmt}"
else:
TypeError("`filename` and `format` cannot both be None.")
filename = os.path.join(dir, filename)
dir = Path(filename).resolve().parent

if mkdir:
outdir = Path(filename).parent.resolve()
if not outdir.exists():
outdir.mkdir(parents=True)

if not format:
format = guess_format(filename, fmap=FMAP)
fmt = revmap.get(format, format)

if overwrite and filename and os.path.exists(filename):
os.remove(filename)

EMMO = rdflib.Namespace( # pylint:disable=invalid-name
"http://emmo.info/emmo#"
)

if recursive:
if squash:
raise ValueError(
"`recursive` and `squash` should not both be true"
)
base = self.base_iri.rstrip("#/")
for onto in self.imported_ontologies:
obase = onto.base_iri.rstrip("#/")
newdir = Path(dir) / os.path.relpath(obase, base)
onto.save(
filename=None,
format=format,
dir=newdir.resolve(),
mkdir=mkdir,
overwrite=overwrite,
recursive=recursive,
squash=squash,
write_catalog_file=write_catalog_file,
append_catalog=append_catalog,
catalog_file=catalog_file,
)

if squash:
from rdflib import ( # pylint:disable=import-outside-toplevel
URIRef,
RDF,
OWL,
)

graph = self.world.as_rdflib_graph()
graph.namespace_manager.bind("emmo", EMMO)

# Remove anonymous namespace and imports
graph.remove((URIRef("http://anonymous"), RDF.type, OWL.Ontology))
imports = list(graph.triples((None, OWL.imports, None)))
for triple in imports:
graph.remove(triple)

graph.serialize(destination=filename, format=format)
elif format in OWLREADY2_FORMATS:
super().save(file=filename, format=fmt)
else:
with tempfile.NamedTemporaryFile(suffix=".owl") as handle:
tmpname = handle.name
super().save(file=tmpname, format="rdfxml", **kwargs)
graph = rdflib.Graph()
graph.parse(tmpname, format="xml")
graph.serialize(destination=filename, format=format)
try:
super().save(file=tmpname, format="rdfxml")
graph = rdflib.Graph()
graph.namespace_manager.bind("emmo", EMMO)
graph.parse(tmpname, format="xml")
graph.serialize(destination=filename, format=format)
finally:
os.remove(tmpname)

if write_catalog_file:
mappings = {}
base = self.base_iri.rstrip("#/")

def append(onto):
obase = onto.base_iri.rstrip("#/")
newdir = Path(dir) / os.path.relpath(obase, base)
newpath = newdir.resolve() / f"{onto.name}.{fmt}"
relpath = os.path.relpath(newpath, dir)
mappings[onto.get_version(as_iri=True)] = str(relpath)
for imported in onto.imported_ontologies:
append(imported)

if recursive:
append(self)
write_catalog(
mappings, output=catalog_file, dir=dir, append=append_catalog
)

def get_imported_ontologies(self, recursive=False):
"""Return a list with imported ontologies.
Expand Down Expand Up @@ -837,23 +955,27 @@ def sync_reasoner(
Keyword arguments are passed to the underlying owlready2 function.
"""
if reasoner == "Pellet":
if reasoner == "FaCT++":
sync = sync_reasoner_factpp
elif reasoner == "Pellet":
sync = owlready2.sync_reasoner_pellet
elif reasoner == "HermiT":
sync = owlready2.sync_reasoner_hermit
elif reasoner == "FaCT++":
sync = sync_reasoner_factpp
else:
raise ValueError(
f"unknown reasoner {reasoner!r}. Supported reasoners are "
'"Pellet", "HermiT" and "FaCT++".'
f"unknown reasoner {reasoner!r}. Supported reasoners "
'are "Pellet", "HermiT" and "FaCT++".'
)

if include_imported:
with self:
sync(**kwargs)
else:
sync([self], **kwargs)
# For some reason we must visit all entities once before running
# the reasoner...
list(self.get_entities())

with self:
if include_imported:
sync(self.world, **kwargs)
else:
sync(self, **kwargs)

def sync_attributes( # pylint: disable=too-many-branches
self,
Expand Down Expand Up @@ -914,11 +1036,10 @@ class prefLabel(owlready2.label):
if not hasattr(ind, "prefLabel"):
# no prefLabel - create new annotation property..
with self:

# pylint: disable=invalid-name,missing-class-docstring
# pylint: disable=function-redefined
class prefLabel(owlready2.label):
pass
iri = "http://www.w3.org/2004/02/skos/core#prefLabel"

ind.prefLabel = [locstr(ind.name, lang="en")]
elif not ind.prefLabel:
Expand Down
Loading

0 comments on commit 5cdb24e

Please sign in to comment.