diff --git a/.github/workflows/cd_publish.yml b/.github/workflows/cd_publish.yml index 2497372bd..aff27a9a6 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/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index 897c4bad8..f919f312b 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 @@ -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' 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/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/).* 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/__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): 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/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/ontopy/ontology.py b/ontopy/ontology.py index de97ce424..c177f6925 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, ) @@ -264,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, @@ -305,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 @@ -393,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}" @@ -1629,14 +1636,50 @@ 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] - ) -> ThingClass: + self, + name: str, + parent: Union[ + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + Iterable, + ], + entitytype: Optional[ + Union[ + str, + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + ] + ] = "class", + ) -> Union[ + ThingClass, + ObjectPropertyClass, + DataPropertyClass, + AnnotationPropertyClass, + ]: """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' (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: + 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. - Throws exception if name consists of more than one word. """ if " " in name: raise LabelDefinitionError( @@ -1644,17 +1687,98 @@ 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: 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. diff --git a/ontopy/patch.py b/ontopy/patch.py index db8b37698..05235d9e3 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 @@ -67,13 +67,51 @@ 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 annotation properties.""" + prop = self.namespace.ontology.get_by_label(name) + if isinstance(prop, AnnotationPropertyClass): + return getattr(self, name) + raise KeyError(f"no such annotation property: {name}") + + +def _setitem(self, name, value): + """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 + 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 annotation 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 +193,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/ontopy/utils.py b/ontopy/utils.py index e36a5f6cf..5ef03df07 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.""" @@ -246,17 +247,19 @@ def fmt(entity): return f"inverse({fmt(expr.property)})" if isinstance(expr, owlready2.disjoint.AllDisjoint): return fmt(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): + 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/pyproject.toml b/pyproject.toml index 8fc9f738c..2a6e73374 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 --ignore=demo --ignore=docs/demo --ignore=examples --ignore=docs/examples --ignore=site" 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", 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/ontopy_tests/test_new_entity.py b/tests/ontopy_tests/test_new_entity.py new file mode 100644 index 000000000..681f8d3e9 --- /dev/null +++ b/tests/ontopy_tests/test_new_entity.py @@ -0,0 +1,102 @@ +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", + ) + 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 + ) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index 5d86dcc58..d6704863f 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 @@ -13,49 +15,62 @@ # 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 set(emmo.Atom["altLabel"]) == {"ChemicalElement"} + +with pytest.raises(KeyError): + emmo.Atom["hasPart"] + +emmo.Atom["altLabel"] = "Element" +assert set(emmo.Atom["altLabel"]) == {"ChemicalElement", "Element"} + +del emmo.Atom["altLabel"] +assert emmo.Atom["altLabel"] == [] + +emmo.Atom["altLabel"] = "ChemicalElement" +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, 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, +# 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, +# } 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..148232338 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -7,9 +7,10 @@ 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 + from ontopy.utils import LabelDefinitionError, EntityClassDefinitionError onto = get_ontology("onto.owl") onto.imported_ontologies.append(emmo) @@ -29,6 +30,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/test_excelparser/onto.xlsx b/tests/test_excelparser/onto.xlsx index 3ff88b994..2274d0018 100755 Binary files a/tests/test_excelparser/onto.xlsx and b/tests/test_excelparser/onto.xlsx differ diff --git a/tests/test_excelparser/onto_only_classes.xlsx b/tests/test_excelparser/onto_only_classes.xlsx new file mode 100755 index 000000000..3ff88b994 Binary files /dev/null and b/tests/test_excelparser/onto_only_classes.xlsx differ diff --git a/tests/test_excelparser/onto_update.xlsx b/tests/test_excelparser/onto_update.xlsx old mode 100755 new mode 100644 index 415c8cc12..f8c36d396 Binary files a/tests/test_excelparser/onto_update.xlsx and b/tests/test_excelparser/onto_update.xlsx differ diff --git a/tests/test_excelparser/onto_update_only_classes.xlsx b/tests/test_excelparser/onto_update_only_classes.xlsx new file mode 100644 index 000000000..f8c36d396 Binary files /dev/null and b/tests/test_excelparser/onto_update_only_classes.xlsx differ diff --git a/tests/test_excelparser/result_ontology/fromexcelonto.ttl b/tests/test_excelparser/result_ontology/fromexcelonto.ttl index 5ded0b43f..4b4c9f7fa 100644 --- a/tests/test_excelparser/result_ontology/fromexcelonto.ttl +++ b/tests/test_excelparser/result_ontology/fromexcelonto.ttl @@ -1,17 +1,17 @@ @prefix : . -@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 9ae141e34..ce006d02c 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 = ( @@ -26,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", 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)) 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 . 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.