diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c40d7059b..0f3719470 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: check-symlinks - id: check-xml @@ -17,7 +17,7 @@ repos: args: [--markdown-linebreak-ext=md] - repo: https://github.com/ambv/black - rev: 24.2.0 + rev: 24.3.0 hooks: - id: black name: Blacken diff --git a/emmopy/emmocheck.py b/emmopy/emmocheck.py index 1df5ca8b7..db177de82 100644 --- a/emmopy/emmocheck.py +++ b/emmopy/emmocheck.py @@ -73,17 +73,30 @@ def test_number_of_labels(self): """ exceptions = set( ( - "terms.license", + "0.1.homepage", # foaf:homepage + "0.1.logo", + "0.1.page", + "0.1.name", + "bibo:doi", + "core.altLabel", + "core.hiddenLabel", + "core.prefLabel", "terms.abstract", + "terms.alternative", + "terms:bibliographicCitation", "terms.contributor", + "terms.created", "terms.creator", + "terms.hasFormat", + "terms.identifier", + "terms.issued", + "terms.license", + "terms.modified", "terms.publisher", + "terms.source", "terms.title", - "core.prefLabel", - "core.altLabel", - "core.hiddenLabel", - "foaf.logo", - "0.1.logo", # foaf.logo + "vann:preferredNamespacePrefix", + "vann:preferredNamespaceUri", ) ) exceptions.update( diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 02e1a217f..47425c771 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -1004,15 +1004,19 @@ def save( # Make a copy of the owlready2 graph object to not mess with # owlready2 internals graph = rdflib.Graph() - graph_owlready2 = self.world.as_rdflib_graph() - for triple in graph_owlready2.triples((None, None, None)): + for triple in self.world.as_rdflib_graph(): graph.add(triple) - # Add namespaces - graph.namespace_manager.bind("", rdflib.Namespace(self.base_iri)) - graph.namespace_manager.bind( - "swrl", rdflib.Namespace("http://www.w3.org/2003/11/swrl#") - ) + # Add common namespaces unknown to rdflib + extra_namespaces = [ + ("", self.base_iri), + ("swrl", "http://www.w3.org/2003/11/swrl#"), + ("bibo", "http://purl.org/ontology/bibo/"), + ] + for prefix, iri in extra_namespaces: + graph.namespace_manager.bind( + prefix, rdflib.Namespace(iri), override=False + ) # Remove all ontology-declarations in the graph that are # not the current ontology. diff --git a/ontopy/utils.py b/ontopy/utils.py index cf759d0b7..010e5e751 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -835,3 +835,38 @@ def recur(o): ) return layout + + +def copy_annotation(onto, src, dst): + """In all classes and properties in `onto`, copy annotation `src` to `dst`. + + Arguments: + onto: Ontology to work on. + src: Name of source annotation. + dst: Name or IRI of destination annotation. Use IRI if the + destination annotation is not already in the ontology. + """ + if onto.world[src]: + src = onto.world[src] + else: + src = onto[src] + + if onto.world[dst]: + dst = onto.world[dst] + elif dst in onto: + dst = onto[dst] + else: + if "://" not in dst: + raise ValueError( + "new destination annotation property must be provided as " + "a full IRI" + ) + name = min(dst.rsplit("#")[-1], dst.rsplit("/")[-1], key=len) + iri = dst + dst = onto.new_annotation_property(name, owlready2.AnnotationProperty) + dst.iri = iri + + for e in onto.get_entities(): + new = getattr(e, src.name).first() + if new and new not in getattr(e, dst.name): + getattr(e, dst.name).append(new) diff --git a/tests/test_get_by_label.py b/tests/test_get_by_label.py index 5b84a5281..669ad9715 100644 --- a/tests/test_get_by_label.py +++ b/tests/test_get_by_label.py @@ -109,8 +109,11 @@ def test_get_by_label_emmo(emmo: "Ontology") -> None: assert emmo[emmo.Atom.iri] == emmo.Atom # Load an ontology with imported sub-ontologies + # Note that this test also includes testing of loading + # ontology with catalog file directly from the web + # It is therefore not duplicated in test_load. onto = get_ontology( - "https://raw.githubusercontent.com/BIG-MAP/BattINFO/master/battinfo.ttl" + "https://raw.githubusercontent.com/emmo-repo/domain-battery/master/battery.ttl" ).load() assert onto.Electrolyte.prefLabel.en.first() == "Electrolyte" diff --git a/tests/test_load.py b/tests/test_load.py index bb5f04223..35c911fd3 100755 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -35,11 +35,8 @@ def test_load(repo_dir: "Path", testonto: "Ontology") -> None: assert str(testonto.TestClass.prefLabel.first()) == "TestClass" # Use catalog file when downloading from web - onto = get_ontology( - "https://raw.githubusercontent.com/BIG-MAP/BattINFO/master/" - "battinfo.ttl" - ).load() - assert str(onto.Electrolyte.prefLabel.first()) == "Electrolyte" + # This is tested in test_get_by_label in which some additional tests + # are performed on labels. with pytest.raises( EMMOntoPyException, diff --git a/tests/testonto/domainonto.ttl b/tests/testonto/domainonto.ttl new file mode 100644 index 000000000..f7e5bf71e --- /dev/null +++ b/tests/testonto/domainonto.ttl @@ -0,0 +1,51 @@ +@prefix : . +@prefix owl: . +@prefix rdf: . +@prefix xml: . +@prefix xsd: . +@prefix rdfs: . +@prefix skos: . +@prefix dcterms: . +@prefix emmo: . + + + rdf:type owl:Ontology ; + owl:versionIRI ; + owl:versionInfo "0.1.0" ; + dcterms:abstract "Test for an EMMO-based domain ontolgoy."@en . + + +:testclass rdf:type owl:Class ; + rdfs:subClassOf owl:Thing ; + skos:prefLabel "TestClass"@en ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "A test class."@en . + +:testobjectproperty rdf:type owl:ObjectProperty ; + rdfs:domain :testclass ; + rdfs:range :testclass ; + skos:prefLabel "hasObjectProperty"@en . + +:testannotationproperty rdf:type owl:AnnotationProperty ; + rdfs:domain :testclass ; + rdfs:range rdfs:Literal ; + skos:prefLabel "hasAnnotationProperty"@en . + +:testdatatypeproperty rdf:type owl:DatatypeProperty ; + rdfs:domain :testclass ; + rdfs:range xsd:string ; + skos:prefLabel "hasDataProperty"@en . + + +# Declare skos:prefLabel, emmo:elucidation and emmo:definition here +# since we are not importing these ontologies +emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 a owl:AnnotationProperty ; + rdfs:subPropertyOf rdfs:comment ; + skos:prefLabel "elucidation"@en ; + rdfs:comment "Short enlightening explanation aimed to facilitate the user in drawing the connection (interpretation) between a OWL entity and the real world object(s) for which it stands."@en . + +:EMMO_70fe84ff_99b6_4206_a9fc_9a8931836d84 a owl:AnnotationProperty ; + rdfs:subPropertyOf rdfs:comment ; + skos:prefLabel "definition"@en ; + rdfs:comment "Precise and univocal description of an ontological entity in the framework of an axiomatic system."@en . + +skos:prefLabel a owl:AnnotationProperty . diff --git a/tests/tools/test_ontoconvert.py b/tests/tools/test_ontoconvert.py index 8a6c1e719..92f4a19c1 100644 --- a/tests/tools/test_ontoconvert.py +++ b/tests/tools/test_ontoconvert.py @@ -39,3 +39,39 @@ def test_run() -> None: assert re.search("@prefix : ", output2) assert re.search(" .* owl:Ontology", output2) assert re.search("testclass .* owl:Class", output2) + + # Test 3 - copy-emmo-annotations + infile3 = ontodir / "domainonto.ttl" + outfile3 = outdir / "test_ontoconvert3.ttl" + ontoconvert.main( + [ + "--copy-emmo-annotations", + "--iri=https://w3id.org/ex/testonto", + "--base-iri=https://w3id.org/ex/testonto#", + str(infile3), + str(outfile3), + ] + ) + input3 = infile3.read_text() + output3 = outfile3.read_text() + assert 'rdfs:label "hasAnnotationProperty"@en' not in input3 + assert 'rdfs:label "hasAnnotationProperty"@en' in output3 + assert 'rdfs:comment "A test class."@en' not in input3 + assert 'rdfs:comment "A test class."@en' in output3 + + # Test 4 - copy-annotation with source as annotation label + infile4 = ontodir / "testonto.ttl" + outfile4 = outdir / "test_ontoconvert3.ttl" + ontoconvert.main( + [ + "-c prefLabel-->http://www.w3.org/2004/02/skos/core#hiddenLabel", + "--iri=https://w3id.org/ex/testonto", + "--base-iri=https://w3id.org/ex/testonto#", + str(infile4), + str(outfile4), + ] + ) + input4 = infile4.read_text() + output4 = outfile4.read_text() + assert not re.search('skos:hiddenLabel "hasAnnotationProperty"@en', input4) + assert re.search('skos:hiddenLabel "hasAnnotationProperty"@en', output4) diff --git a/tools/ontoconvert b/tools/ontoconvert index 934d12df8..4e06cdf79 100755 --- a/tools/ontoconvert +++ b/tools/ontoconvert @@ -7,7 +7,7 @@ import warnings from rdflib.util import guess_format from ontopy import get_ontology -from ontopy.utils import annotate_source, rename_iris +from ontopy.utils import annotate_source, rename_iris, copy_annotation def main(argv: list = None): @@ -59,6 +59,36 @@ def main(argv: list = None): "The default is to append to it." ), ) + parser.add_argument( + "--copy-annotation", + "-c", + action="append", + default=[], + metavar="FROM-->TO", + help=( + "Copy annotation FROM to annotation TO in each class and " + "property in the ontology. FROM and TO may be given as " + "full IRIs or (if they already exists as annotations in the " + "ontology) as entity names. " + "This option be given multiple times." + ), + ) + parser.add_argument( + "--copy-emmo-annotations", + "-e", + action="store_true", + help=( + "Make a copy of EMMO annotations to plain RDFS for increased " + "interoperability. " + "Alias for: `--copy-annotation=" + "http://www.w3.org/2004/02/skos/core#prefLabel" + "-->http://www.w3.org/2000/01/rdf-schema#label " + "--copy-annotation=elucidation" + "-->http://www.w3.org/2000/01/rdf-schema#comment`" + "--copy-annotation=definition" + "-->http://www.w3.org/2000/01/rdf-schema#comment`" + ), + ) parser.add_argument( "--no-catalog", "-n", @@ -72,7 +102,7 @@ def main(argv: list = None): "--infer", "-i", nargs="?", - const="FaCT++", + const="HermiT", choices=["HermiT", "Pellet", "FaCT++"], metavar="NAME", help=( @@ -188,6 +218,17 @@ def main(argv: list = None): if not output_format: output_format = "xml" + # Annotations to copy with --copy-emmo-annotations + if args.copy_emmo_annotations: + args.copy_annotation.extend( + [ + "http://www.w3.org/2004/02/skos/core#prefLabel" + "-->http://www.w3.org/2000/01/rdf-schema#label", + "elucidation-->http://www.w3.org/2000/01/rdf-schema#comment", + "definition-->http://www.w3.org/2000/01/rdf-schema#comment", + ] + ) + # Perform conversion with warnings.catch_warnings(record=True) as warnings_handle: warnings.simplefilter("always") @@ -218,6 +259,10 @@ def main(argv: list = None): debug=verbose, ) + for cpy in args.copy_annotation: + src, dst = cpy.split("-->", 1) + copy_annotation(onto, src.strip(), dst.strip()) + onto.save( args.output, format=output_format,