From 25a9d908b55403bad9fc0650e92bf156b15c7517 Mon Sep 17 00:00:00 2001 From: EMMOntoPy Developers Date: Wed, 24 May 2023 06:35:50 +0000 Subject: [PATCH 01/25] Update `pre-commit` hooks --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 18d6699c9..c25984225 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,7 +36,7 @@ repos: # exclude: ^tests/.*$ - repo: https://github.com/SINTEF/ci-cd - rev: v2.3.1 + rev: v2.4.0 hooks: - id: docs-api-reference args: From 518518b23a615cbd9061ac2671a8dc6322e7ec30 Mon Sep 17 00:00:00 2001 From: EMMOntoPy Developers Date: Wed, 24 May 2023 08:10:17 +0000 Subject: [PATCH 02/25] Release v0.5.2 - Changelog --- CHANGELOG.md | 49 ++++++++++++++++++++++++++++++++++++++++++++-- ontopy/__init__.py | 2 +- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3619bbc0b..c4146f5a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,55 @@ ## [Unreleased](https://github.com/emmo-repo/EMMOntoPy/tree/HEAD) -[Full Changelog](https://github.com/emmo-repo/EMMOntoPy/compare/v0.5.1...HEAD) +[Full Changelog](https://github.com/emmo-repo/EMMOntoPy/compare/v0.5.2...HEAD) + +**Closed issues:** + +- Harmonize get\_descendants and get\_ancestors [\#406](https://github.com/emmo-repo/EMMOntoPy/issues/406) + +## [v0.5.2](https://github.com/emmo-repo/EMMOntoPy/tree/v0.5.2) (2023-05-12) + +[Full Changelog](https://github.com/emmo-repo/EMMOntoPy/compare/v0.5.1...v0.5.2) **Fixed bugs:** -- Use custom token for GitHub changelog generator [\#545](https://github.com/emmo-repo/EMMOntoPy/issues/545) +- Auto-merge dependabot PRs workflow invalid [\#566](https://github.com/emmo-repo/EMMOntoPy/issues/566) + +**Closed issues:** + +- Point to excelparser api from the tools-page [\#593](https://github.com/emmo-repo/EMMOntoPy/issues/593) +- BUG: pytest - missing remote file /0.5.0/electrochemicalquantities / ontology [\#589](https://github.com/emmo-repo/EMMOntoPy/issues/589) +- Owlready 0.41 support ? [\#588](https://github.com/emmo-repo/EMMOntoPy/issues/588) +- Allow space in labels [\#583](https://github.com/emmo-repo/EMMOntoPy/issues/583) +- is\_defined needs a better description [\#563](https://github.com/emmo-repo/EMMOntoPy/issues/563) +- utils line 112 in get\_iri\_name link = "{lowerlabel}" vs "{label}" [\#562](https://github.com/emmo-repo/EMMOntoPy/issues/562) +- ontograph - update colour deafults [\#559](https://github.com/emmo-repo/EMMOntoPy/issues/559) +- ontograph - argument leafs should be leaves [\#558](https://github.com/emmo-repo/EMMOntoPy/issues/558) +- ontograph - write out more examples on how to use it [\#557](https://github.com/emmo-repo/EMMOntoPy/issues/557) +- ontograph --parents not working [\#556](https://github.com/emmo-repo/EMMOntoPy/issues/556) +- test\_graph2 is failing [\#555](https://github.com/emmo-repo/EMMOntoPy/issues/555) +- Add client side redirection in generated html documentation [\#552](https://github.com/emmo-repo/EMMOntoPy/issues/552) +- Typos in PR template [\#523](https://github.com/emmo-repo/EMMOntoPy/issues/523) +- ontograph, read format from name [\#497](https://github.com/emmo-repo/EMMOntoPy/issues/497) +- Review default colours and style in ontopy/graph.py [\#345](https://github.com/emmo-repo/EMMOntoPy/issues/345) + +**Merged pull requests:** + +- Add links to the original FaCT++ repo, GitHub profiles, etc. [\#600](https://github.com/emmo-repo/EMMOntoPy/pull/600) ([blokhin](https://github.com/blokhin)) +- Added test update to PR template. [\#598](https://github.com/emmo-repo/EMMOntoPy/pull/598) ([jesper-friis](https://github.com/jesper-friis)) +- Changed `is_defined` into a ThingClass property and improved its documentation. [\#597](https://github.com/emmo-repo/EMMOntoPy/pull/597) ([jesper-friis](https://github.com/jesper-friis)) +- Added link to excelparser from tools for documentation of excel sheet. [\#594](https://github.com/emmo-repo/EMMOntoPy/pull/594) ([francescalb](https://github.com/francescalb)) +- Bump SINTEF/ci-cd from 2.3.0 to 2.3.1 [\#584](https://github.com/emmo-repo/EMMOntoPy/pull/584) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Updated get\_by\_label\(\) so that it now accepts label, name and full iri [\#582](https://github.com/emmo-repo/EMMOntoPy/pull/582) ([jesper-friis](https://github.com/jesper-friis)) +- Added two additional exceptions to emmocheck [\#577](https://github.com/emmo-repo/EMMOntoPy/pull/577) ([jesper-friis](https://github.com/jesper-friis)) +- Bump SINTEF/ci-cd from 2.2.1 to 2.3.0 [\#575](https://github.com/emmo-repo/EMMOntoPy/pull/575) ([dependabot[bot]](https://github.com/apps/dependabot)) +- get\_ancestors and get\_descendants have the same arguments. [\#572](https://github.com/emmo-repo/EMMOntoPy/pull/572) ([francescalb](https://github.com/francescalb)) +- Bump SINTEF/ci-cd from 2.2.0 to 2.2.1 [\#571](https://github.com/emmo-repo/EMMOntoPy/pull/571) ([dependabot[bot]](https://github.com/apps/dependabot)) +- ontograph: colour updates, examples, bugfix [\#569](https://github.com/emmo-repo/EMMOntoPy/pull/569) ([francescalb](https://github.com/francescalb)) +- Bump SINTEF/ci-cd from 2.1.0 to 2.2.0 [\#567](https://github.com/emmo-repo/EMMOntoPy/pull/567) ([dependabot[bot]](https://github.com/apps/dependabot)) +- Changed argument leafs to leaves, with deprecation warning in ontograph [\#564](https://github.com/emmo-repo/EMMOntoPy/pull/564) ([francescalb](https://github.com/francescalb)) +- Corrected bug on getting default relation style. [\#561](https://github.com/emmo-repo/EMMOntoPy/pull/561) ([francescalb](https://github.com/francescalb)) +- Fix internal links in generated documentation generated with ontodoc [\#548](https://github.com/emmo-repo/EMMOntoPy/pull/548) ([jesper-friis](https://github.com/jesper-friis)) ## [v0.5.1](https://github.com/emmo-repo/EMMOntoPy/tree/v0.5.1) (2023-02-07) @@ -14,6 +58,7 @@ **Fixed bugs:** +- Use custom token for GitHub changelog generator [\#545](https://github.com/emmo-repo/EMMOntoPy/issues/545) - Avoid using Azure mirror for APT packages [\#541](https://github.com/emmo-repo/EMMOntoPy/issues/541) **Merged pull requests:** diff --git a/ontopy/__init__.py b/ontopy/__init__.py index 358ef825f..090b53f73 100644 --- a/ontopy/__init__.py +++ b/ontopy/__init__.py @@ -2,7 +2,7 @@ # pylint: disable=wrong-import-position,wrong-import-order import sys -__version__ = "0.5.1" +__version__ = "0.5.2" # Ensure correct Python version if sys.version_info < (3, 7): From 0c992ad73de140f3beaa5c74bd87d9abf13bac3b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 24 May 2023 16:30:50 +0200 Subject: [PATCH 03/25] Added DOI badge --- README.md | 1 + docs/index.md | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index 19ff453eb..c28466901 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,7 @@ ![CI tests](https://github.com/emmo-repo/EMMOntoPy/workflows/CI%20Tests/badge.svg) [![PyPI version](https://badge.fury.io/py/EMMOntoPy.svg)](https://badge.fury.io/py/EMMOntoPy) +[![DOI](https://zenodo.org/badge/190286064.svg)](https://zenodo.org/badge/latestdoi/190286064) > ***Note**: EMMOntoPy is a continuation of the EMMO-python project and the associated `emmo` Python package. > To see the legacy versions go to [PyPI](https://pypi.org/project/EMMO/).* diff --git a/docs/index.md b/docs/index.md index 60fc5252e..2107265f1 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ ![CI tests](https://github.com/emmo-repo/EMMOntoPy/workflows/CI%20Tests/badge.svg) [![PyPI version](https://badge.fury.io/py/EMMOntoPy.svg)](https://badge.fury.io/py/EMMOntoPy) +[![DOI](https://zenodo.org/badge/190286064.svg)](https://zenodo.org/badge/latestdoi/190286064) > ***Note**: EMMOntoPy is a continuation of the EMMO-python project and the associated `emmo` Python package. > To see the legacy versions go to [PyPI](https://pypi.org/project/EMMO/).* From 25007dc4f1af998e2ec2ae03566b7aa621aeafca Mon Sep 17 00:00:00 2001 From: francescalb Date: Tue, 30 May 2023 19:45:11 +0200 Subject: [PATCH 04/25] Changed add entity to not only include classes. Now also includes dataproperties, objectoproperties and objectproperties. The default behavious of add entity has not changed. --- ontopy/ontology.py | 64 +++++++++++++++--- ontopy/utils.py | 23 ++++--- tests/ontopy_tests/test_new_entity.py | 94 +++++++++++++++++++++++++++ tests/ontopy_tests/test_prefix.py | 13 ++-- tests/test_basic.py | 5 +- tests/testonto/testonto.ttl | 15 +++++ 6 files changed, 192 insertions(+), 22 deletions(-) create mode 100644 tests/ontopy_tests/test_new_entity.py diff --git a/ontopy/ontology.py b/ontopy/ontology.py index de97ce424..9468b14cd 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -23,6 +23,8 @@ import owlready2 from owlready2 import locstr from owlready2.entity import ThingClass +from owlready2.prop import ObjectPropertyClass, DataPropertyClass +from owlready2 import AnnotationPropertyClass from ontopy.factpluspluswrapper.sync_factpp import sync_reasoner_factpp from ontopy.utils import ( @@ -39,7 +41,7 @@ ReadCatalogError, _validate_installed_version, LabelDefinitionError, - ThingClassDefinitionError, + EntityClassDefinitionError, EMMOntoPyException, ) @@ -1629,14 +1631,38 @@ def get_wu_palmer_measure(self, cls1, cls2): return 2 * ccadepth / (generations1 + generations2 + 2 * ccadepth) def new_entity( - self, name: str, parent: Union[ThingClass, Iterable] + self, + name: str, + parent: Union[ + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + Iterable, + ], + entitytype: Optional[ + Union[ + str, + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + ] + ] = "class", ) -> ThingClass: """Create and return new entity - Makes a new entity in the ontology with given parent(s). - Return the new entity. + Args: + name: name of the entity + parent: parent(s) of the entity + entitytype: type of the entity, default is 'class'.Other options + are data_property, object_property, annotation_property. + + Returns: + the new entity. - Throws exception if name consists of more than one word. + Throws exception if name consists of more than one word, if type is not + one of the allowed types, or if parent is not of the correct type. """ if " " in name: raise LabelDefinitionError( @@ -1644,11 +1670,33 @@ def new_entity( f"Label consists of more than one word." ) parents = tuple(parent) if isinstance(parent, Iterable) else (parent,) + + if entitytype == "class": + parenttype = owlready2.ThingClass + elif entitytype == "data_property": + parenttype = owlready2.DataPropertyClass + elif entitytype == "object_property": + parenttype = owlready2.ObjectPropertyClass + elif entitytype == "annotation_property": + parenttype = owlready2.AnnotationPropertyClass + elif entitytype in [ + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + ]: + parenttype = entitytype + else: + raise EntityClassDefinitionError( + f"Error in entity type definition: " + f"'{entitytype}' is not a valid entity type." + ) + for thing in parents: - if not isinstance(thing, owlready2.ThingClass): - raise ThingClassDefinitionError( + if not isinstance(thing, parenttype): + raise EntityClassDefinitionError( f"Error in parent definition: " - f"'{thing}' is not an owlready2.ThingClass." + f"'{thing}' is not an {parenttype}." ) with self: diff --git a/ontopy/utils.py b/ontopy/utils.py index e36a5f6cf..3292c674f 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -5,6 +5,7 @@ import sys import re import datetime +import inspect import tempfile from pathlib import Path from typing import TYPE_CHECKING @@ -69,7 +70,7 @@ class LabelDefinitionError(EMMOntoPyException): """Error in label definition.""" -class ThingClassDefinitionError(EMMOntoPyException): +class EntityClassDefinitionError(EMMOntoPyException): """Error in ThingClass definition.""" @@ -136,6 +137,7 @@ def asstring( # pylint: disable=too-many-return-statements,too-many-branches,to """ if ontology is None: ontology = expr.ontology + print("exprtio", expr) def fmt(entity): """Returns the formatted label of an entity.""" @@ -246,17 +248,20 @@ def fmt(entity): return f"inverse({fmt(expr.property)})" if isinstance(expr, owlready2.disjoint.AllDisjoint): return fmt(expr) + print("expr1", expr) if isinstance(expr, (bool, int, float)): return repr(expr) # Check for subclasses - if issubclass(expr, (bool, int, float, str)): - return fmt(expr.__class__.__name__) - if issubclass(expr, datetime.date): - return "date" - if issubclass(expr, datetime.time): - return "datetime" - if issubclass(expr, datetime.datetime): - return "datetime" + if inspect.isclass(expr): + print("expr", expr, type(expr)) + if issubclass(expr, (bool, int, float, str)): + return fmt(expr.__class__.__name__) + if issubclass(expr, datetime.date): + return "date" + if issubclass(expr, datetime.time): + return "datetime" + if issubclass(expr, datetime.datetime): + return "datetime" raise RuntimeError(f"Unknown expression: {expr!r} (type: {type(expr)!r})") diff --git a/tests/ontopy_tests/test_new_entity.py b/tests/ontopy_tests/test_new_entity.py new file mode 100644 index 000000000..51321d3e5 --- /dev/null +++ b/tests/ontopy_tests/test_new_entity.py @@ -0,0 +1,94 @@ +from typing import TYPE_CHECKING +import pytest +from ontopy.utils import ( + NoSuchLabelError, + LabelDefinitionError, + EntityClassDefinitionError, +) +from owlready2.entity import ThingClass +from owlready2.prop import ObjectPropertyClass, DataPropertyClass +from owlready2 import AnnotationPropertyClass + +if TYPE_CHECKING: + from pathlib import Path + + +def test_new_entity(testonto: "Ontology") -> None: + """Test adding entities to ontology""" + + # Add entity directly + testonto.new_entity("FantasyClass", testonto.TestClass) + + # Test that new entity is found by both version of get_by_label + assert testonto.get_by_label("FantasyClass") == testonto.FantasyClass + assert testonto.get_by_label_all("FantasyClass") == [testonto.FantasyClass] + + testonto.sync_attributes() + # Test that after sync_attributes, the entity is not counted more than once + assert testonto.get_by_label_all("FantasyClass") == [testonto.FantasyClass] + + with pytest.raises(LabelDefinitionError): + testonto.new_entity("Fantasy Class", testonto.TestClass) + + testonto.new_entity( + "AnotherClass", testonto.TestClass, entitytype=ThingClass + ) + testonto.new_entity( + "hasSubObjectProperty", + testonto.hasObjectProperty, + entitytype=ObjectPropertyClass, + ) + testonto.new_entity( + "hasSubDataProperty", + testonto.hasDataProperty, + entitytype=DataPropertyClass, + ) + testonto.new_entity( + "hasSubAnnotationProperty", + testonto.hasAnnotationProperty, + entitytype=AnnotationPropertyClass, + ) + + testonto.sync_attributes() + testonto.new_entity( + "AnotherClass2", testonto.AnotherClass, entitytype="class" + ) + testonto.new_entity( + "hasSubObjectProperty2", + testonto.hasSubObjectProperty, + entitytype="object_property", + ) + testonto.new_entity( + "hasSubDataProperty2", + testonto.hasSubDataProperty, + entitytype="data_property", + ) + testonto.new_entity( + "hasSubAnnotationProperty2", + testonto.hasSubAnnotationProperty, + entitytype="annotation_property", + ) + + with pytest.raises(EntityClassDefinitionError): + testonto.new_entity("FantasyClass", testonto.hasObjectProperty) + + with pytest.raises(EntityClassDefinitionError): + testonto.new_entity( + "hasSubProperty", + testonto.hasObjectProperty, + entitytype="data_property", + ) + + with pytest.raises(EntityClassDefinitionError): + testonto.new_entity( + "hasSubProperty", + testonto.hasObjectProperty, + entitytype=AnnotationPropertyClass, + ) + + with pytest.raises(EntityClassDefinitionError): + testonto.new_entity( + "hasSubProperty", + testonto.hasObjectProperty, + entitytype="nonexistingpropertytype", + ) diff --git a/tests/ontopy_tests/test_prefix.py b/tests/ontopy_tests/test_prefix.py index a2d8e713d..fb7e9ca54 100644 --- a/tests/ontopy_tests/test_prefix.py +++ b/tests/ontopy_tests/test_prefix.py @@ -9,10 +9,15 @@ def test_prefix(testonto: "Ontology", emmo: "Ontology") -> None: """Test prefix in ontology""" - assert len(testonto.get_by_label_all("*")) == 3 - assert testonto.get_by_label_all("*", prefix="testonto") == [ - testonto.TestClass - ] + assert len(testonto.get_by_label_all("*")) == 6 + assert set(testonto.get_by_label_all("*", prefix="testonto")) == set( + [ + testonto.hasObjectProperty, + testonto.TestClass, + testonto.hasAnnotationProperty, + testonto.hasDataProperty, + ] + ) assert ( testonto.get_by_label("TestClass", prefix="testonto") == testonto.TestClass diff --git a/tests/test_basic.py b/tests/test_basic.py index 1be4e8eb0..e78846b39 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -9,7 +9,7 @@ def test_basic(emmo: "Ontology") -> None: from ontopy import get_ontology - from ontopy.utils import LabelDefinitionError + from ontopy.utils import LabelDefinitionError, EntityClassDefinitionError onto = get_ontology("onto.owl") onto.imported_ontologies.append(emmo) @@ -29,6 +29,9 @@ def test_basic(emmo: "Ontology") -> None: with pytest.raises(LabelDefinitionError): onto.new_entity("Hydr ogen", emmo.Atom) + with pytest.raises(EntityClassDefinitionError): + onto.new_entity("Hydrogen", emmo.hasPart) + with onto: # Add entity using python classes class Oxygen(emmo.Atom): diff --git a/tests/testonto/testonto.ttl b/tests/testonto/testonto.ttl index 1e75e59e3..9bf091c4b 100644 --- a/tests/testonto/testonto.ttl +++ b/tests/testonto/testonto.ttl @@ -16,3 +16,18 @@ :testclass rdf:type owl:Class ; rdfs:subClassOf owl:Thing ; skos:prefLabel "TestClass"@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 . From 1fa625da16597e62e90f4f8483af2426bea8852e Mon Sep 17 00:00:00 2001 From: francescalb Date: Tue, 30 May 2023 19:48:58 +0200 Subject: [PATCH 05/25] Iproved description --- ontopy/ontology.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 9468b14cd..14a294a24 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -1655,14 +1655,19 @@ def new_entity( Args: name: name of the entity parent: parent(s) of the entity - entitytype: type of the entity, default is 'class'.Other options - are data_property, object_property, annotation_property. + entitytype: type of the entity, + default is 'class/ThingClass'. Other options + are data_property, object_property, + annotation_property or ObjectPropertyClass, + DataPropertyClass and AnnotationProperty classes. Returns: the new entity. Throws exception if name consists of more than one word, if type is not one of the allowed types, or if parent is not of the correct type. + By default, the parent is Thing. + """ if " " in name: raise LabelDefinitionError( From 8322e8c208eb928e38a27713b4b7391136fb5faa Mon Sep 17 00:00:00 2001 From: francescalb Date: Tue, 30 May 2023 20:36:10 +0200 Subject: [PATCH 06/25] Corrected docstring --- ontopy/ontology.py | 75 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 14a294a24..1322cec0c 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -1649,17 +1649,22 @@ def new_entity( AnnotationPropertyClass, ] ] = "class", - ) -> ThingClass: + ) -> Union[ + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + ]: """Create and return new entity Args: name: name of the entity parent: parent(s) of the entity entitytype: type of the entity, - default is 'class/ThingClass'. Other options - are data_property, object_property, - annotation_property or ObjectPropertyClass, - DataPropertyClass and AnnotationProperty classes. + default is 'class/ThingClass'. Other options + are data_property, object_property, + annotation_property or ObjectPropertyClass, + DataPropertyClass and AnnotationProperty classes. Returns: the new entity. @@ -1708,6 +1713,66 @@ def new_entity( entity = types.new_class(name, parents) return entity + # Method that creates new ThingClass using new_entity + def new_class( + self, name: str, parent: Union[ThingClass, Iterable] + ) -> ThingClass: + """Create and return new class. + + Args: + name: name of the class + parent: parent(s) of the class + + Returns: + the new class. + """ + return self.new_entity(name, parent, "class") + + # Method that creates new ObjectPropertyClass using new_entity + def new_object_property( + self, name: str, parent: Union[ObjectPropertyClass, Iterable] + ) -> ObjectPropertyClass: + """Create and return new object property. + + Args: + name: name of the object property + parent: parent(s) of the object property + + Returns: + the new object property. + """ + return self.new_entity(name, parent, "object_property") + + # Method that creates new DataPropertyClass using new_entity + def new_data_property( + self, name: str, parent: Union[DataPropertyClass, Iterable] + ) -> DataPropertyClass: + """Create and return new data property. + + Args: + name: name of the data property + parent: parent(s) of the data property + + Returns: + the new data property. + """ + return self.new_entity(name, parent, "data_property") + + # Method that creates new AnnotationPropertyClass using new_entity + def new_annotation_property( + self, name: str, parent: Union[AnnotationPropertyClass, Iterable] + ) -> AnnotationPropertyClass: + """Create and return new annotation property. + + Args: + name: name of the annotation property + parent: parent(s) of the annotation property + + Returns: + the new annotation property. + """ + return self.new_entity(name, parent, "annotation_property") + class BlankNode: """Represents a blank node. From 9798b9325d88f83129a7e297cac2431241b123b5 Mon Sep 17 00:00:00 2001 From: francescalb Date: Tue, 30 May 2023 21:01:48 +0200 Subject: [PATCH 07/25] Added some checks required by codecod --- ontopy/utils.py | 2 +- tests/ontopy_tests/test_new_entity.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/ontopy/utils.py b/ontopy/utils.py index 3292c674f..5180a7acf 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -248,7 +248,7 @@ def fmt(entity): return f"inverse({fmt(expr.property)})" if isinstance(expr, owlready2.disjoint.AllDisjoint): return fmt(expr) - print("expr1", expr) + if isinstance(expr, (bool, int, float)): return repr(expr) # Check for subclasses diff --git a/tests/ontopy_tests/test_new_entity.py b/tests/ontopy_tests/test_new_entity.py index 51321d3e5..681f8d3e9 100644 --- a/tests/ontopy_tests/test_new_entity.py +++ b/tests/ontopy_tests/test_new_entity.py @@ -92,3 +92,11 @@ def test_new_entity(testonto: "Ontology") -> None: testonto.hasObjectProperty, entitytype="nonexistingpropertytype", ) + testonto.new_class("AnotherClass3", (testonto.AnotherClass,)) + testonto.new_object_property( + "hasSubObjectProperty3", testonto.hasObjectProperty + ) + testonto.new_data_property("hasSubDataProperty3", testonto.hasDataProperty) + testonto.new_annotation_property( + "hasSubAnnotationProperty3", testonto.hasAnnotationProperty + ) From adb03ab36cb4f55a8d3f89488d0d6024fbb4c91a Mon Sep 17 00:00:00 2001 From: francescalb Date: Wed, 31 May 2023 08:39:55 +0200 Subject: [PATCH 08/25] More infomrative info on allowed entitytypes --- ontopy/ontology.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 1322cec0c..3a18de8f0 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -1661,9 +1661,11 @@ def new_entity( name: name of the entity parent: parent(s) of the entity entitytype: type of the entity, - default is 'class/ThingClass'. Other options - are data_property, object_property, - annotation_property or ObjectPropertyClass, + default is 'class' (str) 'ThingClass' (owlready2 Python class). + Other options + are 'data_property', 'object_property', + 'annotation_property' (strings) or the + Python classes ObjectPropertyClass, DataPropertyClass and AnnotationProperty classes. Returns: From f95c4bd56e3c6a8b69d86c60c7509d342d832cb7 Mon Sep 17 00:00:00 2001 From: "Francesca L. Bleken" <48128015+francescalb@users.noreply.github.com> Date: Wed, 31 May 2023 09:37:00 +0200 Subject: [PATCH 09/25] Update ontopy/utils.py --- ontopy/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ontopy/utils.py b/ontopy/utils.py index 5180a7acf..9706a6684 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -253,7 +253,6 @@ def fmt(entity): return repr(expr) # Check for subclasses if inspect.isclass(expr): - print("expr", expr, type(expr)) if issubclass(expr, (bool, int, float, str)): return fmt(expr.__class__.__name__) if issubclass(expr, datetime.date): From 08338fce17e84aca1ebac60e637a44df857b19d3 Mon Sep 17 00:00:00 2001 From: "Francesca L. Bleken" <48128015+francescalb@users.noreply.github.com> Date: Wed, 31 May 2023 09:48:05 +0200 Subject: [PATCH 10/25] Update ontopy/utils.py --- ontopy/utils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/ontopy/utils.py b/ontopy/utils.py index 9706a6684..5ef03df07 100644 --- a/ontopy/utils.py +++ b/ontopy/utils.py @@ -137,7 +137,6 @@ def asstring( # pylint: disable=too-many-return-statements,too-many-branches,to """ if ontology is None: ontology = expr.ontology - print("exprtio", expr) def fmt(entity): """Returns the formatted label of an entity.""" From dab55a275fef302ebe46a022429972c5c7b2796d Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Fri, 2 Jun 2023 19:32:59 +0200 Subject: [PATCH 11/25] Added item access, assignment and deletion to Thing's. --- ontopy/patch.py | 47 ++++++++++++++++++++++++++++++-- tests/ontopy_tests/test_patch.py | 14 ++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index db8b37698..7bd782871 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -5,7 +5,7 @@ import owlready2 from owlready2 import ThingClass, PropertyClass, Thing, Restriction, Namespace from owlready2 import Metadata -from ontopy.utils import EMMOntoPyException +from ontopy.utils import EMMOntoPyException, get_label def render_func(entity): @@ -67,13 +67,53 @@ def get_parents(self, strict=False): def _dir(self): - """Extend in dir() listing of ontology classes.""" + """Extend dir() listing of ontology classes.""" set_dir = set(object.__dir__(self)) props = self.namespace.world._props.keys() set_dir.update(props) return sorted(set_dir) +def _getitem(self, name): + """Provide item access to properties.""" + onto = self.namespace.ontology + labels = {get_label(prop) for prop in onto.properties()} + # labels.update(('is_a', 'equivalent_to', 'disjoint_unions')) + if name in labels: + return getattr(self, name) + raise KeyError(f"no such property: {name}") + + +def _setitem(self, name, value): + """Provide item asignment for properties. + + Note, this appends `value` to the property instead of replacing the + property. This is consistent with Owlready2, but may be little + unintuitive. + + Example: + >>> from emmopy import get_emmo + >>> emmo = get_emmo() + >>> emmo.Atom['altLabel'] + ['ChemicalElement'] + emmo.Atom['altLabel'] = 'Element' + >>> emmo.Atom['altLabel'] + ['ChemicalElement', 'Element'] + + """ + item = _getitem(self, name) + item.append(value) + + +def _delitem(self, name): + """Provide item deletion for properties. + + Note, this simply clears the named property. + """ + item = _getitem(self, name) + item.clear() + + def get_annotations( self, all=False, imported=True ): # pylint: disable=redefined-builtin @@ -155,6 +195,9 @@ def get_indirect_is_a(self, skip_classes=True): # Inject methods into ThingClass setattr(ThingClass, "__dir__", _dir) +setattr(ThingClass, "__getitem__", _getitem) +setattr(ThingClass, "__setitem__", _setitem) +setattr(ThingClass, "__delitem__", _delitem) setattr(ThingClass, "get_preferred_label", get_preferred_label) setattr(ThingClass, "get_parents", get_parents) setattr(ThingClass, "get_annotations", get_annotations) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index 5d86dcc58..340bf5a6b 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -25,6 +25,20 @@ "elucidation", } + +# Test item access/assignment/deletion for ThingClass +assert emmo.Atom["altLabel"] == ["ChemicalElement"] + +emmo.Atom["altLabel"] = "Element" +assert emmo.Atom["altLabel"] == ["ChemicalElement", "Element"] + +del emmo.Atom["altLabel"] +assert emmo.Atom["altLabel"] == [] + +emmo.Atom["altLabel"] = "ChemicalElement" +assert emmo.Atom["altLabel"] == ["ChemicalElement"] + + # TODO: Fix disjoint_with(). # It seems not to take into account disjoint unions. # assert set(emmo.Collection.disjoint_with()) == {emmo.Item} From b9c3d50694f57cd9fdc48d34da35c28505083342 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 00:20:09 +0200 Subject: [PATCH 12/25] Simplified implementation of patched __getitem__ method on ThingClass. --- ontopy/patch.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index 7bd782871..bc2c4b677 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -5,7 +5,7 @@ import owlready2 from owlready2 import ThingClass, PropertyClass, Thing, Restriction, Namespace from owlready2 import Metadata -from ontopy.utils import EMMOntoPyException, get_label +from ontopy.utils import EMMOntoPyException def render_func(entity): @@ -76,10 +76,8 @@ def _dir(self): def _getitem(self, name): """Provide item access to properties.""" - onto = self.namespace.ontology - labels = {get_label(prop) for prop in onto.properties()} - # labels.update(('is_a', 'equivalent_to', 'disjoint_unions')) - if name in labels: + prop = self.namespace.ontology.get_by_label(name) + if isinstance(prop, PropertyClass): return getattr(self, name) raise KeyError(f"no such property: {name}") From f81dfe5a9be707935d2e30c49973e3b8e1eac43e Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 11:29:38 +0200 Subject: [PATCH 13/25] Restricted item access for classes from properties to just annotations --- ontopy/patch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index bc2c4b677..14b467619 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -3,8 +3,8 @@ import types import owlready2 -from owlready2 import ThingClass, PropertyClass, Thing, Restriction, Namespace -from owlready2 import Metadata +from owlready2 import AnnotationPropertyClass, ThingClass, PropertyClass +from owlready2 import Metadata, Thing, Restriction, Namespace from ontopy.utils import EMMOntoPyException @@ -75,15 +75,15 @@ def _dir(self): def _getitem(self, name): - """Provide item access to properties.""" + """Provide item access to annotation properties.""" prop = self.namespace.ontology.get_by_label(name) - if isinstance(prop, PropertyClass): + if isinstance(prop, AnnotationPropertyClass): return getattr(self, name) - raise KeyError(f"no such property: {name}") + raise KeyError(f"no such annotation property: {name}") def _setitem(self, name, value): - """Provide item asignment for properties. + """Provide item asignment for annotation properties. Note, this appends `value` to the property instead of replacing the property. This is consistent with Owlready2, but may be little @@ -104,7 +104,7 @@ def _setitem(self, name, value): def _delitem(self, name): - """Provide item deletion for properties. + """Provide item deletion for annotation properties. Note, this simply clears the named property. """ From d1d699e806d4ebef0bf88f1b13ad4844558ce517 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 12:18:19 +0200 Subject: [PATCH 14/25] Added doctest --- pyproject.toml | 3 +++ tests/ontopy_tests/test_graph.py | 3 +++ tests/test_basic.py | 1 + tests/test_excelparser/test_excelparser.py | 8 +++++++- tests/tools/test_excel2onto.py | 3 +++ 5 files changed, 17 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8fc9f738c..5a49cb994 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,3 +10,6 @@ disable = [ [tool.pylint.format] max-line-length = "80" + +[tool.pytest.ini_options] +addopts = "--doctest-modules" diff --git a/tests/ontopy_tests/test_graph.py b/tests/ontopy_tests/test_graph.py index 0be76f9ec..afd9747a9 100644 --- a/tests/ontopy_tests/test_graph.py +++ b/tests/ontopy_tests/test_graph.py @@ -7,6 +7,9 @@ from ontopy.ontology import Ontology +@pytest.mark.filterwarnings( + "ignore:Style not defined for relation hasSpecialRelation:UserWarning" +) def test_graph(testonto: "Ontology", tmpdir: "Path") -> None: """Testing OntoGraph on a small ontology diff --git a/tests/test_basic.py b/tests/test_basic.py index e78846b39..148232338 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -7,6 +7,7 @@ from ontopy.ontology import Ontology +@pytest.mark.filterwarnings("ignore:adding new IRI to ontology:UserWarning") def test_basic(emmo: "Ontology") -> None: from ontopy import get_ontology from ontopy.utils import LabelDefinitionError, EntityClassDefinitionError diff --git a/tests/test_excelparser/test_excelparser.py b/tests/test_excelparser/test_excelparser.py index 9ae141e34..81448c9a2 100644 --- a/tests/test_excelparser/test_excelparser.py +++ b/tests/test_excelparser/test_excelparser.py @@ -1,7 +1,8 @@ """Test the Excel parser module.""" -import pytest from typing import TYPE_CHECKING +import pytest + from ontopy import get_ontology from ontopy.excelparser import create_ontology_from_excel from ontopy.utils import NoSuchLabelError @@ -10,6 +11,11 @@ from pathlib import Path +@pytest.mark.filterwarnings("ignore:Ignoring concept :UserWarning") +@pytest.mark.filterwarnings("ignore:Invalid parents for :UserWarning") +@pytest.mark.filterwarnings( + "ignore:Not able to add the following concepts :UserWarning" +) def test_excelparser(repo_dir: "Path") -> None: """Basic test for creating an ontology from an Excel file.""" ontopath = ( diff --git a/tests/tools/test_excel2onto.py b/tests/tools/test_excel2onto.py index 6c8859c9b..cd81bf325 100644 --- a/tests/tools/test_excel2onto.py +++ b/tests/tools/test_excel2onto.py @@ -1,12 +1,15 @@ """Test the `ontograph` tool.""" from typing import TYPE_CHECKING +import pytest + if TYPE_CHECKING: from pathlib import Path from types import ModuleType from typing import Callable +@pytest.mark.filterwarnings("ignore::UserWarning") def test_run(get_tool: "Callable[[str], ModuleType]", tmpdir: "Path") -> None: """Check that running `excel2onto` works. From d8feef71362ce087208b6d9985fee759d347fe81 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 12:54:20 +0200 Subject: [PATCH 15/25] Added doctest to ci testing as well... --- .github/workflows/ci_workflow.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index 1c01b0521..c729d02b5 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -88,7 +88,7 @@ jobs: pip install -U -e .[dev] - name: Test - run: pytest -vvv --cov=ontopy --cov=emmopy --cov-report=xml --cov-report=term + run: pytest -vvv --cov=ontopy --cov=emmopy --cov-report=xml --cov-report=term --doctest-modules - name: Upload coverage to Codecov if: matrix.python-version == '3.7' && github.repository == 'emmo-repo/EMMOntoPy' From 8797844694017807f2f3945373db1b22b7fe3932 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 12:59:51 +0200 Subject: [PATCH 16/25] Also test Python 3.11 and support it officially --- .github/workflows/ci_workflow.yml | 2 +- setup.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index 1c01b0521..d2d20dc69 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -67,7 +67,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] steps: - name: Checkout repository diff --git a/setup.py b/setup.py index b2d024b46..b02c9b7ac 100644 --- a/setup.py +++ b/setup.py @@ -95,6 +95,7 @@ def fglob(patt): "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Visualization", From 68f146de3e4fcc0eb967861a56ca413de944a36a Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 14:11:58 +0200 Subject: [PATCH 17/25] Corrected doctest in manchester.py Added folders that we want pytest to ignore --- examples/ontology-from-excel/make_microstructure_onto.py | 6 +++++- ontopy/manchester.py | 2 +- pyproject.toml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/ontology-from-excel/make_microstructure_onto.py b/examples/ontology-from-excel/make_microstructure_onto.py index de0171fe5..9e29fe073 100755 --- a/examples/ontology-from-excel/make_microstructure_onto.py +++ b/examples/ontology-from-excel/make_microstructure_onto.py @@ -1,11 +1,15 @@ """ python example for creating ontology from excel """ +from pathlib import Path + from ontopy.excelparser import create_ontology_from_excel from ontopy.utils import write_catalog + +thisdir = Path(__file__).resolve().parent ontology, catalog, errdict = create_ontology_from_excel( - "tool/microstructure.xlsx" + thisdir / "tool/microstructure.xlsx" ) ontology.save("microstructure_ontology.ttl", format="turtle", overwrite=True) diff --git a/ontopy/manchester.py b/ontopy/manchester.py index 4f7fdc6e9..143196f76 100644 --- a/ontopy/manchester.py +++ b/ontopy/manchester.py @@ -94,7 +94,7 @@ def evaluate(ontology: owlready2.Ontology, expr: str) -> owlready2.Construct: Example: >>> from ontopy.manchester import evaluate >>> from ontopy import get_ontology - >>> emmo = get_ontology.load() + >>> emmo = get_ontology().load() >>> restriction = evaluate(emmo, 'hasPart some Atom') >>> cls = evaluate(emmo, 'Atom') diff --git a/pyproject.toml b/pyproject.toml index 5a49cb994..2a6e73374 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,4 +12,4 @@ disable = [ max-line-length = "80" [tool.pytest.ini_options] -addopts = "--doctest-modules" +addopts = "--doctest-modules --ignore=demo --ignore=docs/demo --ignore=examples --ignore=docs/examples --ignore=site" From 1dfecf950c22d056678201d18f066d642e29062b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Sat, 3 Jun 2023 14:25:58 +0200 Subject: [PATCH 18/25] Full test coverage --- tests/ontopy_tests/test_patch.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index 340bf5a6b..6457a78a6 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -2,6 +2,8 @@ Implemented as a script, such that it easy to understand and use for debugging. """ +import pytest + from ontopy import get_ontology from owlready2 import owl, Inverse @@ -26,9 +28,12 @@ } -# Test item access/assignment/deletion for ThingClass +# Test item access/assignment/deletion for classes assert emmo.Atom["altLabel"] == ["ChemicalElement"] +with pytest.raises(KeyError): + emmo.Atom["hasPart"] + emmo.Atom["altLabel"] = "Element" assert emmo.Atom["altLabel"] == ["ChemicalElement", "Element"] From 4818e9a4ea8a276bb9d1333916be081d044a9bf2 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Tue, 6 Jun 2023 10:29:43 +0200 Subject: [PATCH 19/25] Fixed doctest --- ontopy/patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index 14b467619..05235d9e3 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -94,7 +94,7 @@ def _setitem(self, name, value): >>> emmo = get_emmo() >>> emmo.Atom['altLabel'] ['ChemicalElement'] - emmo.Atom['altLabel'] = 'Element' + >>> emmo.Atom['altLabel'] = 'Element' >>> emmo.Atom['altLabel'] ['ChemicalElement', 'Element'] From 587a9a2b22e947469b7832f4b832a933e5bd287b Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 7 Jun 2023 17:38:31 +0200 Subject: [PATCH 20/25] Fixed test_patch.py --- tests/ontopy_tests/test_patch.py | 67 +++++++++++++++----------------- 1 file changed, 31 insertions(+), 36 deletions(-) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index 6457a78a6..f85dd2a72 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -15,27 +15,24 @@ # Test some ThingClass extensions implemented in patch.py assert emmo.Atom.get_preferred_label() == "Atom" -assert emmo.Atom.get_parents() == { - emmo.CausalSystem, - emmo.CompositeParticle, - emmo.MolecularEntity, -} +assert emmo.Atom.get_parents() == {emmo.MolecularEntity} assert set(emmo.Atom.get_annotations().keys()) == { "prefLabel", "altLabel", "elucidation", + "comment", } # Test item access/assignment/deletion for classes -assert emmo.Atom["altLabel"] == ["ChemicalElement"] +assert set(emmo.Atom["altLabel"]) == {"ChemicalElement"} with pytest.raises(KeyError): emmo.Atom["hasPart"] emmo.Atom["altLabel"] = "Element" -assert emmo.Atom["altLabel"] == ["ChemicalElement", "Element"] +assert set(emmo.Atom["altLabel"]) == {"ChemicalElement", "Element"} del emmo.Atom["altLabel"] assert emmo.Atom["altLabel"] == [] @@ -44,37 +41,35 @@ assert emmo.Atom["altLabel"] == ["ChemicalElement"] +assert emmo.Atom.is_defined == False +assert emmo.Holistic.is_defined == True + # TODO: Fix disjoint_with(). # It seems not to take into account disjoint unions. # assert set(emmo.Collection.disjoint_with()) == {emmo.Item} -assert set(str(s) for s in emmo.CausalChain.get_indirect_is_a()) == set( - str(s) - for s in { - Inverse(emmo.hasPart).value(emmo.universe), - emmo.CausalObject, - emmo.Particle, - emmo.hasPart.some(emmo.Quantum), - emmo.hasTemporalPart.only(emmo.CausalChain | emmo.Quantum), - emmo.hasTemporalPart.some(emmo.CausalChain | emmo.Quantum), - } -) -assert set( - str(s) for s in emmo.CausalChain.get_indirect_is_a(skip_classes=False) -) == set( - str(s) - for s in { - Inverse(emmo.hasPart).value(emmo.universe), - emmo.CausalObject, - emmo.EMMO, - emmo.Item, - emmo.Particle, - emmo.hasPart.some(emmo.Quantum), - emmo.hasTemporalPart.only(emmo.CausalChain | emmo.Quantum), - emmo.hasTemporalPart.some(emmo.CausalChain | emmo.Quantum), - owl.Thing, - } -) -assert emmo.Atom.is_defined == False -assert emmo.Holistic.is_defined == True +# Comment out these tests for now because Owlready2 automatically converts +# `Inverse(emmo.hasPart)` to `emmo.isPartOf`. +# +# Also, decide whether we really want to keep get_indirect_is_a() - it +# very much dublicates what ancestors() already does. +# assert emmo.CausalChain.get_indirect_is_a() == { +# Inverse(emmo.hasPart).value(emmo.universe), +# emmo.CausalParticle, +# emmo.CausalStructure, +# emmo.hasPart.some(emmo.Quantum), +# emmo.hasTemporalPart.only(emmo.CausalPath | emmo.Quantum), +# emmo.hasTemporalPart.some(emmo.CausalPath | emmo.Quantum), +# } +# assert emmo.CausalChain.get_indirect_is_a(skip_classes=False) == { +# Inverse(emmo.hasPart).value(emmo.universe), +# emmo.CausalObject, +# emmo.EMMO, +# emmo.Item, +# emmo.Particle, +# emmo.hasPart.some(emmo.Quantum), +# emmo.hasTemporalPart.only(emmo.CausalChain | emmo.Quantum), +# emmo.hasTemporalPart.some(emmo.CausalChain | emmo.Quantum), +# owl.Thing, +# } From 0375b08aec914b23587570171f12daba19de069f Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 7 Jun 2023 18:12:40 +0200 Subject: [PATCH 21/25] Updated comment --- tests/ontopy_tests/test_patch.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index f85dd2a72..d6704863f 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -52,8 +52,9 @@ # Comment out these tests for now because Owlready2 automatically converts # `Inverse(emmo.hasPart)` to `emmo.isPartOf`. # -# Also, decide whether we really want to keep get_indirect_is_a() - it -# very much dublicates what ancestors() already does. +# Also, check whether ancestors() does any inferences from disjoint unions, etc. +# If it does, it might be better to reley on ancestors() instead of implementing +# get_indirect_is_a() as a separate method # assert emmo.CausalChain.get_indirect_is_a() == { # Inverse(emmo.hasPart).value(emmo.universe), # emmo.CausalParticle, From 76e61c0b8545cd664336a5700ce2a71259ac647f Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 7 Jun 2023 18:22:32 +0200 Subject: [PATCH 22/25] Fixed change hasSymbolData -> hasSymbolValue --- tests/test_manchester.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/test_manchester.py b/tests/test_manchester.py index c5b6db6ab..bd401f1bc 100644 --- a/tests/test_manchester.py +++ b/tests/test_manchester.py @@ -58,16 +58,16 @@ def test_manchester(): Inverse(emmo.hasPart).value(emmo.universe), ) # literal data restriction - check('hasSymbolData value "hello"', emmo.hasSymbolData.value("hello")) - check("hasSymbolData value 42", emmo.hasSymbolData.value(42)) - check("hasSymbolData value 3.14", emmo.hasSymbolData.value(3.14)) + check('hasSymbolValue value "hello"', emmo.hasSymbolValue.value("hello")) + check("hasSymbolValue value 42", emmo.hasSymbolValue.value(42)) + check("hasSymbolValue value 3.14", emmo.hasSymbolValue.value(3.14)) check( - 'hasSymbolData value "abc"^^xsd:string', - emmo.hasSymbolData.value("abc"), + 'hasSymbolValue value "abc"^^xsd:string', + emmo.hasSymbolValue.value("abc"), ) check( - 'hasSymbolData value "hello"@en', - emmo.hasSymbolData.value(locstr("hello", "en")), + 'hasSymbolValue value "hello"@en', + emmo.hasSymbolValue.value(locstr("hello", "en")), ) check("emmo:hasPart some emmo:Atom", emmo.hasPart.some(emmo.Atom)) From b4edd722555d5116834cc342ff786181f5183db3 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 7 Jun 2023 19:55:26 +0200 Subject: [PATCH 23/25] Made example ugly just to make it pass... --- ontopy/patch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index 05235d9e3..4ab02feda 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -92,11 +92,11 @@ def _setitem(self, name, value): Example: >>> from emmopy import get_emmo >>> emmo = get_emmo() - >>> emmo.Atom['altLabel'] - ['ChemicalElement'] + >>> set(emmo.Atom['altLabel']) + {'ChemicalElement'} >>> emmo.Atom['altLabel'] = 'Element' - >>> emmo.Atom['altLabel'] - ['ChemicalElement', 'Element'] + >>> set(emmo.Atom['altLabel']) + {'ChemicalElement', 'Element'} """ item = _getitem(self, name) From 976fe72a218f7af0b0b8978531bbdb856a9dd4a0 Mon Sep 17 00:00:00 2001 From: Jesper Friis Date: Wed, 7 Jun 2023 22:15:12 +0200 Subject: [PATCH 24/25] Cleaned up... --- ontopy/patch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ontopy/patch.py b/ontopy/patch.py index 4ab02feda..05235d9e3 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -92,11 +92,11 @@ def _setitem(self, name, value): Example: >>> from emmopy import get_emmo >>> emmo = get_emmo() - >>> set(emmo.Atom['altLabel']) - {'ChemicalElement'} + >>> emmo.Atom['altLabel'] + ['ChemicalElement'] >>> emmo.Atom['altLabel'] = 'Element' - >>> set(emmo.Atom['altLabel']) - {'ChemicalElement', 'Element'} + >>> emmo.Atom['altLabel'] + ['ChemicalElement', 'Element'] """ item = _getitem(self, name) From ffb491957f26ba6a2baaede0d22f5f2c4078d0d6 Mon Sep 17 00:00:00 2001 From: "Francesca L. Bleken" <48128015+francescalb@users.noreply.github.com> Date: Thu, 8 Jun 2023 21:55:47 +0200 Subject: [PATCH 25/25] Support for data, object and annotation properties Moved adding of concepts to help_function _add_entities, which can be used to add entities fo type owlread2.Thing/ObjectProperty/AnnotationProperty/DataProperty. _add_entities adds annotation properties, but not relations for classes or ranges and domains for properties Data properties, object properties and annotation properties should be provided in separate sheets called AnnotationProperties, DataProperties, ObjectProperties If these sheets do not exist in the excel workbook they will not be considered. The same if the sheets exist, but do not have prefLabels. An extra column for OtherAnnotations has been added and it is now read correctly. The dictionary with errors is now returned with separate keys for the different properties and classes. --- .github/workflows/cd_publish.yml | 1 + ontopy/excelparser.py | 701 +++++++++++++----- ontopy/ontology.py | 18 +- tests/test_excelparser/onto.xlsx | Bin 22265 -> 24080 bytes tests/test_excelparser/onto_only_classes.xlsx | Bin 0 -> 22265 bytes tests/test_excelparser/onto_update.xlsx | Bin 11640 -> 13032 bytes .../onto_update_only_classes.xlsx | Bin 0 -> 13032 bytes .../result_ontology/fromexcelonto.ttl | 100 ++- .../fromexcelonto_only_classes.ttl | 143 ++++ tests/test_excelparser/test_excelparser.py | 87 ++- 10 files changed, 825 insertions(+), 225 deletions(-) create mode 100755 tests/test_excelparser/onto_only_classes.xlsx mode change 100755 => 100644 tests/test_excelparser/onto_update.xlsx create mode 100644 tests/test_excelparser/onto_update_only_classes.xlsx create mode 100644 tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl diff --git a/.github/workflows/cd_publish.yml b/.github/workflows/cd_publish.yml index 8445fafd0..9fcdad17f 100644 --- a/.github/workflows/cd_publish.yml +++ b/.github/workflows/cd_publish.yml @@ -29,6 +29,7 @@ jobs: python_version_docs: "3.7" doc_extras: "[docs]" changelog_exclude_labels: dependencies + warnings_as_errors: false secrets: PyPI_token: ${{ secrets.PYPI_TOKEN }} diff --git a/ontopy/excelparser.py b/ontopy/excelparser.py index 13bdcbd76..9d2980a2c 100755 --- a/ontopy/excelparser.py +++ b/ontopy/excelparser.py @@ -35,11 +35,14 @@ def english(string): return owlready2.locstr(string, lang="en") -def create_ontology_from_excel( # pylint: disable=too-many-arguments +def create_ontology_from_excel( # pylint: disable=too-many-arguments, too-many-locals excelpath: str, concept_sheet_name: str = "Concepts", metadata_sheet_name: str = "Metadata", imports_sheet_name: str = "ImportedOntologies", + dataproperties_sheet_name: str = "DataProperties", + objectproperties_sheet_name: str = "ObjectProperties", + annotationproperties_sheet_name: str = "AnnotationProperties", base_iri: str = "http://emmo.info/emmo/domain/onto#", base_iri_from_metadata: bool = True, imports: list = None, @@ -68,6 +71,21 @@ def create_ontology_from_excel( # pylint: disable=too-many-arguments Column name is 'Imported ontologies'. Fully resolvable URL or path to imported ontologies provided one per row. + dataproperties_sheet_name: Name of sheet where data properties are + defined. The second row of this sheet should contain column names + that are supported. Currently these are 'prefLabel','altLabel', + 'Elucidation', 'Comments', 'Examples', 'subPropertyOf', + 'Domain', 'Range', 'dijointWith', 'equivalentTo'. + annotationproperties_sheet_name: Name of sheet where annotation + properties are defined. The second row of this sheet should contain + column names that are supported. Currently these are 'prefLabel', + 'altLabel', 'Elucidation', 'Comments', 'Examples', 'subPropertyOf', + 'Domain', 'Range'. + objectproperties_sheet_name: Name of sheet where object properties are + defined.The second row of this sheet should contain column names + that are supported. Currently these are 'prefLabel','altLabel', + 'Elucidation', 'Comments', 'Examples', 'subPropertyOf', + 'Domain', 'Range', 'inverseOf', 'dijointWith', 'equivalentTo'. base_iri: Base IRI of the new ontology. base_iri_from_metadata: Whether to use base IRI defined from metadata. imports: List of imported ontologies. @@ -138,9 +156,69 @@ def _relative_to_absolute_paths(path): conceptdata = pd.read_excel( excelpath, sheet_name=concept_sheet_name, skiprows=[0, 2] ) + try: + objectproperties = pd.read_excel( + excelpath, sheet_name=objectproperties_sheet_name, skiprows=[0, 2] + ) + if "prefLabel" not in objectproperties.columns: + warnings.warn( + "The 'prefLabel' column is missing in " + f"{objectproperties_sheet_name}. " + "New object properties will not be added to the ontology." + ) + objectproperties = None + except ValueError: + warnings.warn( + f"No sheet named {objectproperties_sheet_name} found " + f"in {excelpath}. " + "New object properties will not be added to the ontology." + ) + objectproperties = None + try: + annotationproperties = pd.read_excel( + excelpath, + sheet_name=annotationproperties_sheet_name, + skiprows=[0, 2], + ) + if "prefLabel" not in annotationproperties.columns: + warnings.warn( + "The 'prefLabel' column is missing in " + f"{annotationproperties_sheet_name}. " + "New annotation properties will not be added to the ontology." + ) + annotationproperties = None + except ValueError: + warnings.warn( + f"No sheet named {annotationproperties_sheet_name} " + f"found in {excelpath}. " + "New annotation properties will not be added to the ontology." + ) + annotationproperties = None + + try: + dataproperties = pd.read_excel( + excelpath, sheet_name=dataproperties_sheet_name, skiprows=[0, 2] + ) + if "prefLabel" not in dataproperties.columns: + warnings.warn( + "The 'prefLabel' column is missing in " + f"{dataproperties_sheet_name}. " + "New data properties will not be added to the ontology." + ) + dataproperties = None + except ValueError: + warnings.warn( + f"No sheet named {dataproperties_sheet_name} found in {excelpath}. " + "New data properties will not be added to the ontology." + ) + dataproperties = None + metadata = pd.read_excel(excelpath, sheet_name=metadata_sheet_name) return create_ontology_from_pandas( data=conceptdata, + objectproperties=objectproperties, + dataproperties=dataproperties, + annotationproperties=annotationproperties, metadata=metadata, imports=imports, base_iri=base_iri, @@ -153,6 +231,9 @@ def _relative_to_absolute_paths(path): def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-branches,too-many-statements,too-many-arguments data: pd.DataFrame, + objectproperties: pd.DataFrame, + annotationproperties: pd.DataFrame, + dataproperties: pd.DataFrame, metadata: pd.DataFrame, imports: pd.DataFrame, base_iri: str = "http://emmo.info/emmo/domain/onto#", @@ -166,16 +247,7 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran Check 'create_ontology_from_excel' for complete documentation. """ - - # Remove lines with empty prefLabel - data = data[data["prefLabel"].notna()] - # Convert all data to string, remove spaces, and finally remove - # additional rows with empty prefLabel. - data = data.astype(str) - data["prefLabel"] = data["prefLabel"].str.strip() - data = data[data["prefLabel"].str.len() > 0] - data.reset_index(drop=True, inplace=True) - + # Get ontology to which new concepts should be added if input_ontology: onto = input_ontology catalog = {} @@ -187,222 +259,143 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran # Set given or default base_iri if base_iri_from_metadata is False. if not base_iri_from_metadata: onto.base_iri = base_iri - labels = set(data["prefLabel"]) - for altlabel in data["altLabel"].str.strip(): - if not altlabel == "nan": - labels.update(altlabel.split(";")) - - # Dictionary with lists of concepts that raise errors - concepts_with_errors = { - "already_defined": [], - "in_imported_ontologies": [], - "wrongly_defined": [], - "missing_parents": [], - "invalid_parents": [], - "nonadded_concepts": [], - "errors_in_properties": [], - } onto.sync_python_names() - with onto: - remaining_rows = set(range(len(data))) - all_added_rows = [] - while remaining_rows: - added_rows = set() - for index in remaining_rows: - row = data.loc[index] - name = row["prefLabel"] - try: - onto.get_by_label(name) - if onto.base_iri in [ - a.namespace.base_iri - for a in onto.get_by_label_all(name) - ]: - if not force: - raise ExcelError( - f'Concept "{name}" already in ontology' - ) - warnings.warn( - f'Ignoring concept "{name}" since it is already in ' - "the ontology." - ) - concepts_with_errors["already_defined"].append(name) - continue - concepts_with_errors["in_imported_ontologies"].append(name) - except (ValueError, TypeError) as err: - warnings.warn( - f'Ignoring concept "{name}". ' - f'The following error was raised: "{err}"' - ) - concepts_with_errors["wrongly_defined"].append(name) - continue - except NoSuchLabelError: - pass - if row["subClassOf"] == "nan": - if not force: - raise ExcelError(f"{row[0]} has no subClassOf") - parent_names = [] # Should be "owl:Thing" - concepts_with_errors["missing_parents"].append(name) - else: - parent_names = str(row["subClassOf"]).split(";") - parents = [] - invalid_parent = False - for parent_name in parent_names: - try: - parent = onto.get_by_label(parent_name.strip()) - except (NoSuchLabelError, ValueError) as exc: - if parent_name not in labels: - if force: - warnings.warn( - f'Invalid parents for "{name}": ' - f'"{parent_name}".' - ) - concepts_with_errors["invalid_parents"].append( - name - ) - break - raise ExcelError( - f'Invalid parents for "{name}": {exc}\n' - "Have you forgotten an imported ontology?" - ) from exc - invalid_parent = True - break - else: - parents.append(parent) - - if invalid_parent: - continue - - if not parents: - parents = [owlready2.Thing] - - try: - concept = onto.new_entity(name, parents) - except LabelDefinitionError: - concepts_with_errors["wrongly_defined"].append(name) - continue - - added_rows.add(index) - # Add elucidation - try: - _add_literal( - row, - concept.elucidation, - "Elucidation", - only_one=True, - ) - except AttributeError as err: - if force: - _add_literal( - row, - concept.comment, - "Elucidation", - only_one=True, - ) - warnings.warn("Elucidation added as comment.") - else: - raise ExcelError( - f"Not able to add elucidations. {err}." - ) from err - - # Add examples - try: - _add_literal( - row, concept.example, "Examples", expected=False - ) - except AttributeError: - if force: - warnings.warn( - "Not able to add examples. " - "Did you forget to import an ontology?." - ) + # Add object properties + if objectproperties is not None: + objectproperties = _clean_dataframe(objectproperties) + ( + onto, + objectproperties_with_errors, + added_objprop_indices, + ) = _add_entities( + onto=onto, + data=objectproperties, + entitytype=owlready2.ObjectPropertyClass, + force=force, + ) - # Add comments - _add_literal(row, concept.comment, "Comments", expected=False) + if annotationproperties is not None: + annotationproperties = _clean_dataframe(annotationproperties) + ( + onto, + annotationproperties_with_errors, + added_annotprop_indices, + ) = _add_entities( + onto=onto, + data=annotationproperties, + entitytype=owlready2.AnnotationPropertyClass, + force=force, + ) - # Add altLabels - try: - _add_literal( - row, concept.altLabel, "altLabel", expected=False - ) - except AttributeError as err: - if force is True: - _add_literal( - row, - concept.label, - "altLabel", - expected=False, - ) - warnings.warn("altLabel added as rdfs.label.") - else: - raise ExcelError( - f"Not able to add altLabels. " f"{err}." - ) from err + if dataproperties is not None: + dataproperties = _clean_dataframe(dataproperties) + ( + onto, + dataproperties_with_errors, + added_dataprop_indices, + ) = _add_entities( + onto=onto, + data=dataproperties, + entitytype=owlready2.DataPropertyClass, + force=force, + ) - remaining_rows.difference_update(added_rows) + onto.sync_attributes( + name_policy="uuid", name_prefix="EMMO_", class_docstring="elucidation" + ) - # Detect infinite loop... - if not added_rows and remaining_rows: - unadded = [data.loc[i].prefLabel for i in remaining_rows] - if force is True: - warnings.warn( - f"Not able to add the following concepts: {unadded}." - " Will continue without these." - ) - remaining_rows = False - concepts_with_errors["nonadded_concepts"] = unadded - else: - raise ExcelError( - f"Not able to add the following concepts: {unadded}." - ) - all_added_rows.extend(added_rows) + # Clean up data frame with new concepts + data = _clean_dataframe(data) + # Add entities + onto, entities_with_errors, added_concept_indices = _add_entities( + onto=onto, data=data, entitytype=owlready2.ThingClass, force=force + ) - # Add properties in a second loop - for index in all_added_rows: + # Add entity properties in a second loop + for index in added_concept_indices: row = data.loc[index] properties = row["Relations"] if properties == "nan": properties = None if isinstance(properties, str): try: - concept = onto.get_by_label(row["prefLabel"].strip()) + entity = onto.get_by_label(row["prefLabel"].strip()) except NoSuchLabelError: pass props = properties.split(";") for prop in props: try: - concept.is_a.append(evaluate(onto, prop.strip())) + entity.is_a.append(evaluate(onto, prop.strip())) except pyparsing.ParseException as exc: warnings.warn( - f"Error in Property assignment for: '{concept}'. " + # This is currently not tested + f"Error in Property assignment for: '{entity}'. " f"Property to be Evaluated: '{prop}'. " f"{exc}" ) - concepts_with_errors["errors_in_properties"].append(name) + entities_with_errors["errors_in_properties"].append( + entity.name + ) except NoSuchLabelError as exc: msg = ( - f"Error in Property assignment for: {concept}. " + f"Error in Property assignment for: {entity}. " f"Property to be Evaluated: {prop}. " f"{exc}" ) if force is True: warnings.warn(msg) - concepts_with_errors["errors_in_properties"].append( - name + entities_with_errors["errors_in_properties"].append( + entity.name ) else: raise ExcelError(msg) from exc + # Add range and domain for object properties + if objectproperties is not None: + onto, objectproperties_with_errors = _add_range_domain( + onto=onto, + properties=objectproperties, + added_prop_indices=added_objprop_indices, + properties_with_errors=objectproperties_with_errors, + force=force, + ) + for key, value in objectproperties_with_errors.items(): + entities_with_errors["obj_prop_" + key] = value + # Add range and domain for annotation properties + if annotationproperties is not None: + onto, annotationproperties_with_errors = _add_range_domain( + onto=onto, + properties=annotationproperties, + added_prop_indices=added_annotprop_indices, + properties_with_errors=annotationproperties_with_errors, + force=force, + ) + for key, value in annotationproperties_with_errors.items(): + entities_with_errors["annot_prop_" + key] = value + + # Add range and domain for data properties + if dataproperties is not None: + onto, dataproperties_with_errors = _add_range_domain( + onto=onto, + properties=dataproperties, + added_prop_indices=added_dataprop_indices, + properties_with_errors=dataproperties_with_errors, + force=force, + ) + for key, value in dataproperties_with_errors.items(): + entities_with_errors["data_prop_" + key] = value + # Synchronise Python attributes to ontology onto.sync_attributes( name_policy="uuid", name_prefix="EMMO_", class_docstring="elucidation" ) onto.dir_label = False - concepts_with_errors = { - key: set(value) for key, value in concepts_with_errors.items() + entities_with_errors = { + key: set(value) for key, value in entities_with_errors.items() } - return onto, catalog, concepts_with_errors + return onto, catalog, entities_with_errors def get_metadata_from_dataframe( # pylint: disable=too-many-locals,too-many-branches,too-many-statements @@ -520,7 +513,6 @@ def _parse_literal( """Helper function to make list ouf strings from ';'-delimited strings in one string. """ - if metadata is True: values = data.loc[data["Metadata name"] == name]["Value"].item() else: @@ -555,3 +547,338 @@ def _add_literal( # pylint: disable=too-many-arguments warnings.warn(f"Missing metadata {name}") else: warnings.warn(f"{data[0]} has no {name}") + + +def _clean_dataframe( + data: pd.DataFrame, +) -> pd.DataFrame: + """Remove lines with empty prefLabel, + convert all data to strings, remove spaces, and finally remove + additional rows with 0-length prefLabel. + """ + data = data[data["prefLabel"].notna()] + data = data.astype(str) + data["prefLabel"] = data["prefLabel"].str.strip() + data = data[data["prefLabel"].str.len() > 0] + data.reset_index(drop=True, inplace=True) + return data + + +def _add_entities( + # pylint: disable=too-many-statements,too-many-branches, too-many-locals + onto: ontopy.ontology.Ontology, + data: pd.DataFrame, + entitytype: Union[ + owlready2.ThingClass, + owlready2.AnnotationPropertyClass, + owlready2.ObjectPropertyClass, + owlready2.DataPropertyClass, + ], + force: bool = False, +) -> Tuple[ontopy.ontology.Ontology, dict, list]: + """Add entities to ontology. + Returns ontology, dictionary with lists of entities that raise errors, + and a list with indices of added rows.""" + labels = set(data["prefLabel"]) + for altlabel in data["altLabel"].str.strip(): + if not altlabel == "nan": + labels.update(altlabel.split(";")) + # Find column name depending on entitytype + if entitytype is owlready2.ThingClass: + rowheader = "subClassOf" + # If entitytype is a subclass of owlready2.PropertyClass + elif entitytype in [ + owlready2.AnnotationPropertyClass, + owlready2.ObjectPropertyClass, + owlready2.DataPropertyClass, + ]: + rowheader = "subPropertyOf" + + # Dictionary with lists of entities that raise errors + entities_with_errors = { + "already_defined": [], + "in_imported_ontologies": [], + "wrongly_defined": [], + f"missing_{rowheader}": [], + f"invalid_{rowheader}": [], + "nonadded_entities": [], + "errors_in_properties": [], + } + + with onto: + remaining_rows = set(range(len(data))) + all_added_rows = [] + while remaining_rows: + added_rows = set() + for index in remaining_rows: + row = data.loc[index] + name = row["prefLabel"] + # Check if entity is already in ontology + try: + onto.get_by_label(name) + if onto.base_iri in [ + a.namespace.base_iri + for a in onto.get_by_label_all(name) + ]: + if not force: + raise ExcelError( + f'Concept "{name}" already in ontology' + ) + warnings.warn( + f'Ignoring concept "{name}" since it is already in ' + "the ontology." + ) + entities_with_errors["already_defined"].append(name) + continue + entities_with_errors["in_imported_ontologies"].append(name) + except (ValueError, TypeError) as err: + warnings.warn( + f'Ignoring concept "{name}". ' + f'The following error was raised: "{err}"' + ) + entities_with_errors["wrongly_defined"].append(name) + continue + except NoSuchLabelError: + pass + + # Find parents + if entitytype is owlready2.ThingClass: + rowheader = "subClassOf" + # If entitytype is a subclass of owlready2.PropertyClass + elif entitytype in [ + owlready2.AnnotationPropertyClass, + owlready2.ObjectPropertyClass, + owlready2.DataPropertyClass, + ]: + rowheader = "subPropertyOf" + + ( + parents, + invalid_parent, + entities_with_errors, + ) = _make_entity_list( + onto, + row, + rowheader, + force, + entities_with_errors, + name, + labels, + ) + if invalid_parent: + continue + if not parents: + if entitytype == owlready2.ThingClass: + parents = [owlready2.Thing] + elif entitytype == owlready2.AnnotationPropertyClass: + parents = [owlready2.AnnotationProperty] + elif entitytype == owlready2.ObjectPropertyClass: + parents = [owlready2.ObjectProperty] + elif entitytype == owlready2.DataPropertyClass: + parents = [owlready2.DataProperty] + + # Add entity + try: + entity = onto.new_entity( + name, parents, entitytype=entitytype + ) + except LabelDefinitionError: + entities_with_errors["wrongly_defined"].append(name) + continue + added_rows.add(index) + # Add elucidation + try: + _add_literal( + row, + entity.elucidation, + "Elucidation", + only_one=True, + ) + except AttributeError as err: + if force: + _add_literal( + row, + entity.comment, + "Elucidation", + only_one=True, + ) + warnings.warn("Elucidation added as comment.") + else: + raise ExcelError( + f"Not able to add elucidations. {err}." + ) from err + + # Add examples + try: + _add_literal( + row, entity.example, "Examples", expected=False + ) + except AttributeError: + if force: + warnings.warn( + "Not able to add examples. " + "Did you forget to import an ontology?." + ) + + # Add comments + _add_literal(row, entity.comment, "Comments", expected=False) + + # Add altLabels + try: + _add_literal( + row, entity.altLabel, "altLabel", expected=False + ) + except AttributeError as err: + if force is True: + _add_literal( + row, + entity.label, + "altLabel", + expected=False, + ) + warnings.warn("altLabel added as rdfs.label.") + else: + raise ExcelError( + f"Not able to add altLabels. " f"{err}." + ) from err + # Add other annotations if any + + if not ( + pd.isna(row["Other annotations"]) + or row["Other annotations"] == "" + or row["Other annotations"] == "nan" + ): + for annotation in row["Other annotations"].split(";"): + key, value = annotation.split("=") + entity[key.strip(" ")] = english(value.strip(" ")) + + remaining_rows.difference_update(added_rows) + # Detect infinite loop... + if not added_rows and remaining_rows: + unadded = [data.loc[i].prefLabel for i in remaining_rows] + if force is True: + warnings.warn( + f"Not able to add the following concepts: {unadded}." + " Will continue without these." + ) + remaining_rows = False + entities_with_errors["nonadded_concepts"] = unadded + else: + raise ExcelError( + f"Not able to add the following concepts: {unadded}." + ) + all_added_rows.extend(added_rows) + + return onto, entities_with_errors, all_added_rows + + +# Helper function for adding range and domain to properties +def _add_range_domain( + onto: owlready2.Ontology, + properties: pd.DataFrame, + added_prop_indices: list, + properties_with_errors: dict, + force: bool = False, +) -> Tuple[owlready2.Ontology, dict]: + """Add range and domain to properties. + + Arguments: + onto: ontology with properties already added, + properties: properties to whcih range and domain are to be added, + added_prop_indices: indices in properties dataframe describing + properties that have been added, + properties_with_errors: dictionary to store properties with errors, + force: if True, will skip properties with errors and add them to + the dictionary. If False errors will cause eception. + + Returns: + onto: ontology with range and domain added to properties, + properties_with_errors: dictionary with properties with errors. + """ + # check if both 'Ranges' and 'Domains' columns are present in dataframe + if ( + "Ranges" not in properties.columns + or "Domains" not in properties.columns + ): + return onto, properties_with_errors + + properties_with_errors["errors_in_range"] = [] + properties_with_errors["errors_in_domain"] = [] + for index in added_prop_indices: + row = properties.loc[index] + try: + prop = onto.get_by_label(row["prefLabel"].strip()) + except NoSuchLabelError: + pass + if row["Ranges"] != "nan": + try: + prop.range = [onto.get_by_label(row["Ranges"].strip())] + except NoSuchLabelError as exc: + msg = ( + f"Error in range assignment for: {prop}. " + f"Range to be Evaluated: {row['Ranges']}. " + f"{exc}" + ) + if force is True: + warnings.warn(msg) + properties_with_errors["errors_in_range"].append(prop.name) + else: + raise ExcelError(msg) from exc + if row["Domains"] != "nan": + try: + prop.domain = [onto.get_by_label(row["Domains"].strip())] + except NoSuchLabelError as exc: + msg = ( + f"Error in domain assignment for: {prop}. " + f"Domain to be Evaluated: {row['Domains']}. " + f"{exc}" + ) + if force is True: + warnings.warn(msg) + properties_with_errors["errors_in_domain"].append(prop.name) + else: + raise ExcelError(msg) from exc + return onto, properties_with_errors + + +def _make_entity_list( # pylint: disable=too-many-arguments + onto: owlready2.Ontology, + row: pd.Series, + rowheader: str, + force: bool, + entities_with_errors: dict, + label: str, + valid_labels: list, +): + """Help function to create a list of entities + from a pd.DataFrame wcich is a str.""" + if row[rowheader] == "nan": + if not force: + raise ExcelError(f"{row[0]} has no {rowheader}") + name_list = [] + entities_with_errors[f"missing_{rowheader}"].append(label) + else: + name_list = str(row[rowheader]).split(";") + concepts = [] + invalid_concept = False + for name in name_list: + try: + concept = onto.get_by_label(name.strip()) + except (NoSuchLabelError, ValueError) as exc: + if name not in valid_labels: + if force: + warnings.warn( + f'Invalid {rowheader} for "{label}": ' f'"{name}".' + ) + entities_with_errors[f"invalid_{rowheader}"].append(label) + break + raise ExcelError( + f'Invalid {rowheader} for "{label}": {exc}\n' + "Have you forgotten an imported ontology?" + ) from exc + invalid_concept = True + break + else: + concepts.append(concept) + + return concepts, invalid_concept, entities_with_errors diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 3a18de8f0..c177f6925 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -266,6 +266,14 @@ def get_unabbreviated_triples( self, subject=subject, predicate=predicate, obj=obj, blank=blank ) + def _set_label_annotations(self): + if self._label_annotations is None: + for iri in DEFAULT_LABEL_ANNOTATIONS: + try: + self.add_label_annotation(iri) + except ValueError: + pass + def get_by_label( self, label: str, @@ -307,12 +315,7 @@ def get_by_label( f"Invalid label definition, must be a string: {label!r}" ) - if self._label_annotations is None: - for iri in DEFAULT_LABEL_ANNOTATIONS: - try: - self.add_label_annotation(iri) - except ValueError: - pass + self._set_label_annotations() if colon_in_label is None: colon_in_label = self._colon_in_label @@ -395,6 +398,8 @@ def get_by_label_all(self, label, label_annotations=None, prefix=None): The current implementation also supports "*" as a wildcard matching any number of characters. This may change in the future. """ + self._set_label_annotations() + if not isinstance(label, str): raise TypeError( f"Invalid label definition, " f"must be a string: {label!r}" @@ -1703,7 +1708,6 @@ def new_entity( f"Error in entity type definition: " f"'{entitytype}' is not a valid entity type." ) - for thing in parents: if not isinstance(thing, parenttype): raise EntityClassDefinitionError( diff --git a/tests/test_excelparser/onto.xlsx b/tests/test_excelparser/onto.xlsx index 3ff88b99403bbab6d9ddecb4eb6b5d83b061f245..2274d00189aaf93d81bfa27d138dc92c49adaeca 100755 GIT binary patch delta 10936 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

1DIOfF#l@?tD_z0)>Z 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 delta 8588 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 YN394gHGBg{tOEUINRQ~P_V?`n1qxvrjsO4v diff --git a/tests/test_excelparser/onto_only_classes.xlsx b/tests/test_excelparser/onto_only_classes.xlsx new file mode 100755 index 0000000000000000000000000000000000000000..3ff88b99403bbab6d9ddecb4eb6b5d83b061f245 GIT binary patch literal 22265 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 w_5gr9B>?bWl;BVE|4iKfZoWqSZ|46@ . -@prefix core: . +@prefix dcterms: . @prefix emmo: . @prefix owl: . @prefix rdfs: . -@prefix term: . +@prefix skos: . a owl:Ontology ; - term:contributor "SINTEF"@en, + dcterms:contributor "SINTEF"@en, "SINTEF Industry"@en ; - term:creator "Francesca L. Bleken"@en, + dcterms:creator "Francesca L. Bleken"@en, "Jesper Friis"@en, "Sylvain Gouttebroze"@en ; - term:title "A test domain ontology"@en ; + dcterms:title "A test domain ontology"@en ; owl:imports , ; owl:versionInfo "0.01"@en . @@ -24,21 +24,38 @@ owl:someValuesFrom emmo:EMMO_d4f7d378_5e3b_468a_baa1_a7e98358cda7 ], :EMMO_138590b8-3333-515d-87ab-717aac8434e6, :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8 ; - core:prefLabel "FiniteTemporalPattern"@en . + skos:prefLabel "FiniteTemporalPattern"@en . :EMMO_080262b7-4f7e-582b-916e-8274c73dd629 a owl:Class ; rdfs:subClassOf ; - core:prefLabel "ANewTestClass"@en . + skos:prefLabel "ANewTestClass"@en . + +:EMMO_0ec801a2-7da4-55ff-906b-c5ccc905bb8d a owl:AnnotationProperty ; + rdfs:subPropertyOf :EMMO_98871837-aa90-5eef-9a56-926ae8beebbb ; + skos:prefLabel "subAnnotation"@en . :EMMO_1c81f1eb-8b94-5e74-96de-1aeacbdb5b93 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "The boundary of a grain"@en ; rdfs:subClassOf :EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 ; - core:prefLabel "GrainBoundary"@en . + skos:prefLabel "GrainBoundary"@en . + +:EMMO_41808a43-529f-5798-b0ed-71ddcb2c5456 a owl:Class ; + emmo:EMMO_c84c6752_6d64_48cc_9500_e54a3c34898d "\"A very secure source\""@en ; + :EMMO_0ec801a2-7da4-55ff-906b-c5ccc905bb8d "\"Another thing\""@en ; + :EMMO_98871837-aa90-5eef-9a56-926ae8beebbb "\"A text about this type of boundary\""@en ; + rdfs:subClassOf :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + skos:prefLabel "SuperSpecialBoundary"@en . + +:EMMO_58de9bf1-4c92-57f0-af37-9ec8129c5db7 a owl:ObjectProperty ; + rdfs:domain :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + rdfs:range :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + rdfs:subPropertyOf :EMMO_a14817a8-a449-5115-8924-b90833317d02 ; + skos:prefLabel "hasSubBoundaryPart"@en . :EMMO_6920d08f-b1e4-5789-9778-f75f4514ef46 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; rdfs:subClassOf owl:Thing ; - core:prefLabel "SpatioTemporalBoundary"@en . + skos:prefLabel "SpatioTemporalBoundary"@en . :EMMO_76b2eb15-3ab7-52b3-ade2-755aa390d63e a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Spatial pattern localized in a volume of space"@en ; @@ -51,26 +68,37 @@ owl:someValuesFrom emmo:EMMO_f1a51559_aa3d_43a0_9327_918039f0dfed ], :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8, :EMMO_5f50f77e-f321-53e3-af76-fe5b0a347479 ; - core:prefLabel "FiniteSpatialPattern"@en . + skos:prefLabel "FiniteSpatialPattern"@en . + +:EMMO_7c8ba943-15cf-5621-98a3-ed1e7e68fee8 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "A special boundary."@en ; + :EMMO_98871837-aa90-5eef-9a56-926ae8beebbb "\"A text about this type of boundary\""@en ; + rdfs:subClassOf :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + skos:prefLabel "SpecialBoundary"@en . + +:EMMO_a198aa47-2eca-5738-a69e-91679676ed2b a owl:DatatypeProperty ; + rdfs:domain emmo:EMMO_4ce76d7f_03f8_45b6_9003_90052a79bfaa ; + rdfs:subPropertyOf :EMMO_4f3d7c7b-1f77-5a91-8151-ddea40d9b4a2 ; + skos:prefLabel "hasPrimeNumberData"@en . :EMMO_b04965e6-a9bb-591f-8f8a-1adcb2c8dc39 a owl:Class ; rdfs:subClassOf emmo:EMMO_21f56795_ee72_4858_b571_11cfaa59c1a8 ; - core:prefLabel "1"@en . + skos:prefLabel "1"@en . :EMMO_e0b20a22-7e6f-5c81-beca-35bc5358e11b a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; rdfs:subClassOf :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8, :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 ; - core:prefLabel "FiniteSpatioTemporalPattern"@en . + skos:prefLabel "FiniteSpatioTemporalPattern"@en . :EMMO_e4e653eb-72cd-5dd6-a428-f506d9679774 a owl:Class ; rdfs:subClassOf ; - core:prefLabel "AnotherNewTestClass"@en . + skos:prefLabel "AnotherNewTestClass"@en . :EMMO_e633d033-2af6-5f04-a706-dab826854fb1 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "The boundary of a subgrain"@en ; rdfs:subClassOf owl:Thing ; - core:prefLabel "SubgrainBoundary"@en . + skos:prefLabel "SubgrainBoundary"@en . :EMMO_e919bd0f-97fb-5d47-92fa-f5756640b6fc a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Our own special molecules"@en ; @@ -79,44 +107,55 @@ owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; owl:someValuesFrom :EMMO_8b758694-7dd3-547a-8589-a835c15a0fb2 ], emmo:EMMO_3397f270_dfc1_4500_8f6f_4d0d85ac5f71 ; - core:prefLabel "SpecialMolecule"@en . + skos:prefLabel "SpecialMolecule"@en . :EMMO_f8ad57d3-6cb5-5628-99e6-eb5915bece3a a owl:Class ; rdfs:subClassOf owl:Thing ; - core:prefLabel "SubSubgrainBoundary"@en . + skos:prefLabel "SubSubgrainBoundary"@en . :EMMO_fb1218a4-b462-5e51-9bed-5b8d394551aa a owl:Class ; rdfs:subClassOf [ a owl:Restriction ; owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; owl:someValuesFrom emmo:EMMO_eb77076b_a104_42ac_a065_798b2d2809ad ], emmo:EMMO_3397f270_dfc1_4500_8f6f_4d0d85ac5f71 ; - core:prefLabel "AnotherSpecialMolecule"@en . + skos:prefLabel "AnotherSpecialMolecule"@en . :EMMO_138590b8-3333-515d-87ab-717aac8434e6 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Pattern with only temporal aspect"@en ; emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Voltage in AC plug"@en ; rdfs:subClassOf owl:Thing ; - core:prefLabel "TemporalPattern"@en . + skos:prefLabel "TemporalPattern"@en . + +:EMMO_4f3d7c7b-1f77-5a91-8151-ddea40d9b4a2 a owl:DatatypeProperty ; + rdfs:domain emmo:EMMO_4ce76d7f_03f8_45b6_9003_90052a79bfaa ; + rdfs:subPropertyOf emmo:EMMO_faf79f53_749d_40b2_807c_d34244c192f4 ; + skos:prefLabel "hasIntegerData"@en . :EMMO_5f50f77e-f321-53e3-af76-fe5b0a347479 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Spatial pattern without regular temporal variations"@en ; emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Infinite grid"@en ; rdfs:subClassOf :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 ; - core:prefLabel "SpatialPattern"@en . + skos:prefLabel "SpatialPattern"@en . :EMMO_8b758694-7dd3-547a-8589-a835c15a0fb2 a owl:Class ; rdfs:subClassOf emmo:EMMO_eb77076b_a104_42ac_a065_798b2d2809ad ; - core:prefLabel "Atom"@en . + skos:prefLabel "Atom"@en . -:EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 a owl:Class ; - emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; - rdfs:subClassOf emmo:EMMO_649bf97b_4397_4005_90d9_219755d92e34 ; - core:prefLabel "Boundary"@en . +:EMMO_98871837-aa90-5eef-9a56-926ae8beebbb a owl:AnnotationProperty ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Where to find the entry in the \"book of boundaries\""@en ; + skos:prefLabel "bookOfBoundariesEntry"@en . + +:EMMO_a14817a8-a449-5115-8924-b90833317d02 a owl:ObjectProperty ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "has a part that is a boundary"@en ; + rdfs:comment "This definition is humbug"@en ; + rdfs:domain :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + rdfs:subPropertyOf emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + skos:prefLabel "hasBoundaryPart"@en . :EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 " "@en ; rdfs:subClassOf :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; - core:prefLabel "SpatialBoundary"@en . + skos:prefLabel "SpatialBoundary"@en . :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 a owl:Class ; emmo:EMMO_21ae69b4_235e_479d_8dd8_4f756f694c1b "A"@en, @@ -124,14 +163,14 @@ "Test"@en ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; rdfs:subClassOf :EMMO_cd254842-c697-55f6-917d-9805c77b9187 ; - core:prefLabel "SpatioTemporalPattern"@en . + skos:prefLabel "SpatioTemporalPattern"@en . :EMMO_cd254842-c697-55f6-917d-9805c77b9187 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "everything that can be perceived or measured"@en ; rdfs:comment " this definition is much broader than definition of pattern such as \"the regular and repeated way in which something happens or is\""@en, "a pattern is defined from a contrast"@en ; rdfs:subClassOf emmo:EMMO_649bf97b_4397_4005_90d9_219755d92e34 ; - core:prefLabel "Pattern"@en . + skos:prefLabel "Pattern"@en . :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8 a owl:Class ; emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Pattern occuring within a boundary in the 4D space"@en ; @@ -140,4 +179,9 @@ owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; owl:someValuesFrom :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ], :EMMO_cd254842-c697-55f6-917d-9805c77b9187 ; - core:prefLabel "FinitePattern"@en . + skos:prefLabel "FinitePattern"@en . + +:EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf emmo:EMMO_649bf97b_4397_4005_90d9_219755d92e34 ; + skos:prefLabel "Boundary"@en . diff --git a/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl b/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl new file mode 100644 index 000000000..5ded0b43f --- /dev/null +++ b/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl @@ -0,0 +1,143 @@ +@prefix : . +@prefix core: . +@prefix emmo: . +@prefix owl: . +@prefix rdfs: . +@prefix term: . + + a owl:Ontology ; + term:contributor "SINTEF"@en, + "SINTEF Industry"@en ; + term:creator "Francesca L. Bleken"@en, + "Jesper Friis"@en, + "Sylvain Gouttebroze"@en ; + term:title "A test domain ontology"@en ; + owl:imports , + ; + owl:versionInfo "0.01"@en . + +:EMMO_0264be35-e8ad-5b35-a1a3-84b37bde22d1 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Temporal pattern occurring in a time interval"@en ; + emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Light house during one night"@en ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty emmo:EMMO_e1097637_70d2_4895_973f_2396f04fa204 ; + owl:someValuesFrom emmo:EMMO_d4f7d378_5e3b_468a_baa1_a7e98358cda7 ], + :EMMO_138590b8-3333-515d-87ab-717aac8434e6, + :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8 ; + core:prefLabel "FiniteTemporalPattern"@en . + +:EMMO_080262b7-4f7e-582b-916e-8274c73dd629 a owl:Class ; + rdfs:subClassOf ; + core:prefLabel "ANewTestClass"@en . + +:EMMO_1c81f1eb-8b94-5e74-96de-1aeacbdb5b93 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "The boundary of a grain"@en ; + rdfs:subClassOf :EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 ; + core:prefLabel "GrainBoundary"@en . + +:EMMO_6920d08f-b1e4-5789-9778-f75f4514ef46 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf owl:Thing ; + core:prefLabel "SpatioTemporalBoundary"@en . + +:EMMO_76b2eb15-3ab7-52b3-ade2-755aa390d63e a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Spatial pattern localized in a volume of space"@en ; + emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Textured surface after etching"@en ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + owl:someValuesFrom :EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 ], + [ a owl:Restriction ; + owl:onProperty emmo:EMMO_e1097637_70d2_4895_973f_2396f04fa204 ; + owl:someValuesFrom emmo:EMMO_f1a51559_aa3d_43a0_9327_918039f0dfed ], + :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8, + :EMMO_5f50f77e-f321-53e3-af76-fe5b0a347479 ; + core:prefLabel "FiniteSpatialPattern"@en . + +:EMMO_b04965e6-a9bb-591f-8f8a-1adcb2c8dc39 a owl:Class ; + rdfs:subClassOf emmo:EMMO_21f56795_ee72_4858_b571_11cfaa59c1a8 ; + core:prefLabel "1"@en . + +:EMMO_e0b20a22-7e6f-5c81-beca-35bc5358e11b a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8, + :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 ; + core:prefLabel "FiniteSpatioTemporalPattern"@en . + +:EMMO_e4e653eb-72cd-5dd6-a428-f506d9679774 a owl:Class ; + rdfs:subClassOf ; + core:prefLabel "AnotherNewTestClass"@en . + +:EMMO_e633d033-2af6-5f04-a706-dab826854fb1 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "The boundary of a subgrain"@en ; + rdfs:subClassOf owl:Thing ; + core:prefLabel "SubgrainBoundary"@en . + +:EMMO_e919bd0f-97fb-5d47-92fa-f5756640b6fc a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Our own special molecules"@en ; + rdfs:comment "Used for our own special purpose"@en ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + owl:someValuesFrom :EMMO_8b758694-7dd3-547a-8589-a835c15a0fb2 ], + emmo:EMMO_3397f270_dfc1_4500_8f6f_4d0d85ac5f71 ; + core:prefLabel "SpecialMolecule"@en . + +:EMMO_f8ad57d3-6cb5-5628-99e6-eb5915bece3a a owl:Class ; + rdfs:subClassOf owl:Thing ; + core:prefLabel "SubSubgrainBoundary"@en . + +:EMMO_fb1218a4-b462-5e51-9bed-5b8d394551aa a owl:Class ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + owl:someValuesFrom emmo:EMMO_eb77076b_a104_42ac_a065_798b2d2809ad ], + emmo:EMMO_3397f270_dfc1_4500_8f6f_4d0d85ac5f71 ; + core:prefLabel "AnotherSpecialMolecule"@en . + +:EMMO_138590b8-3333-515d-87ab-717aac8434e6 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Pattern with only temporal aspect"@en ; + emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Voltage in AC plug"@en ; + rdfs:subClassOf owl:Thing ; + core:prefLabel "TemporalPattern"@en . + +:EMMO_5f50f77e-f321-53e3-af76-fe5b0a347479 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Spatial pattern without regular temporal variations"@en ; + emmo:EMMO_b432d2d5_25f4_4165_99c5_5935a7763c1a "Infinite grid"@en ; + rdfs:subClassOf :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 ; + core:prefLabel "SpatialPattern"@en . + +:EMMO_8b758694-7dd3-547a-8589-a835c15a0fb2 a owl:Class ; + rdfs:subClassOf emmo:EMMO_eb77076b_a104_42ac_a065_798b2d2809ad ; + core:prefLabel "Atom"@en . + +:EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf emmo:EMMO_649bf97b_4397_4005_90d9_219755d92e34 ; + core:prefLabel "Boundary"@en . + +:EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 " "@en ; + rdfs:subClassOf :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; + core:prefLabel "SpatialBoundary"@en . + +:EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 a owl:Class ; + emmo:EMMO_21ae69b4_235e_479d_8dd8_4f756f694c1b "A"@en, + "Just"@en, + "Test"@en ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf :EMMO_cd254842-c697-55f6-917d-9805c77b9187 ; + core:prefLabel "SpatioTemporalPattern"@en . + +:EMMO_cd254842-c697-55f6-917d-9805c77b9187 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "everything that can be perceived or measured"@en ; + rdfs:comment " this definition is much broader than definition of pattern such as \"the regular and repeated way in which something happens or is\""@en, + "a pattern is defined from a contrast"@en ; + rdfs:subClassOf emmo:EMMO_649bf97b_4397_4005_90d9_219755d92e34 ; + core:prefLabel "Pattern"@en . + +:EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "Pattern occuring within a boundary in the 4D space"@en ; + rdfs:comment "Every physical patterns are FinitePattern"@en ; + rdfs:subClassOf [ a owl:Restriction ; + owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + owl:someValuesFrom :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ], + :EMMO_cd254842-c697-55f6-917d-9805c77b9187 ; + core:prefLabel "FinitePattern"@en . diff --git a/tests/test_excelparser/test_excelparser.py b/tests/test_excelparser/test_excelparser.py index 81448c9a2..ce006d02c 100644 --- a/tests/test_excelparser/test_excelparser.py +++ b/tests/test_excelparser/test_excelparser.py @@ -32,14 +32,95 @@ def test_excelparser(repo_dir: "Path") -> None: repo_dir / "tests" / "test_excelparser" / "onto_update.xlsx" ) ontology, catalog, errors = create_ontology_from_excel(xlspath, force=True) - assert onto == ontology + assert errors.keys() == { + "already_defined", + "in_imported_ontologies", + "wrongly_defined", + "missing_subClassOf", + "invalid_subClassOf", + "nonadded_entities", + "errors_in_properties", + "nonadded_concepts", + "obj_prop_already_defined", + "obj_prop_in_imported_ontologies", + "obj_prop_wrongly_defined", + "obj_prop_missing_subPropertyOf", + "obj_prop_invalid_subPropertyOf", + "obj_prop_nonadded_entities", + "obj_prop_errors_in_properties", + "obj_prop_errors_in_range", + "obj_prop_errors_in_domain", + "annot_prop_already_defined", + "annot_prop_in_imported_ontologies", + "annot_prop_wrongly_defined", + "annot_prop_missing_subPropertyOf", + "annot_prop_invalid_subPropertyOf", + "annot_prop_nonadded_entities", + "annot_prop_errors_in_properties", + "data_prop_already_defined", + "data_prop_in_imported_ontologies", + "data_prop_wrongly_defined", + "data_prop_missing_subPropertyOf", + "data_prop_invalid_subPropertyOf", + "data_prop_nonadded_entities", + "data_prop_errors_in_properties", + "data_prop_errors_in_range", + "data_prop_errors_in_domain", + } + assert errors["already_defined"] == {"Pattern"} + assert errors["in_imported_ontologies"] == {"Atom"} + assert errors["wrongly_defined"] == {"Temporal Boundary"} + assert errors["missing_subClassOf"] == {"SpatioTemporalBoundary"} + assert errors["invalid_subClassOf"] == { + "TemporalPattern", + "SubSubgrainBoundary", + "SubgrainBoundary", + } + assert errors["nonadded_concepts"] == { + "Pattern", + "Temporal Boundary", + } + + assert len(ontology.get_by_label_all("Atom")) == 2 + with pytest.raises(NoSuchLabelError): + onto.ATotallyNewPattern + + updated_onto, _, _ = create_ontology_from_excel( + update_xlspath, force=True, input_ontology=ontology + ) + assert updated_onto.ATotallyNewPattern + assert updated_onto.Pattern.iri == onto.Pattern.iri + assert len(list(onto.classes())) + 1 == len(list(updated_onto.classes())) + + +def test_excelparser_only_classes(repo_dir: "Path") -> None: + """This loads the excelfile used and tests that the resulting ontology prior + to version 0.5.2 in which only classes where considered, but with empty sheets + for properties.""" + ontopath = ( + repo_dir + / "tests" + / "test_excelparser" + / "result_ontology" + / "fromexcelonto_only_classes.ttl" + ) + onto = get_ontology(str(ontopath)).load() + xlspath = repo_dir / "tests" / "test_excelparser" / "onto_only_classes.xlsx" + update_xlspath = ( + repo_dir + / "tests" + / "test_excelparser" + / "onto_update_only_classes.xlsx" + ) + ontology, catalog, errors = create_ontology_from_excel(xlspath, force=True) + assert onto == ontology assert errors["already_defined"] == {"Pattern"} assert errors["in_imported_ontologies"] == {"Atom"} assert errors["wrongly_defined"] == {"Temporal Boundary"} - assert errors["missing_parents"] == {"SpatioTemporalBoundary"} - assert errors["invalid_parents"] == { + assert errors["missing_subClassOf"] == {"SpatioTemporalBoundary"} + assert errors["invalid_subClassOf"] == { "TemporalPattern", "SubSubgrainBoundary", "SubgrainBoundary",