diff --git a/.github/workflows/cd_publish.yml b/.github/workflows/cd_publish.yml index f017b7c9b..e122a032c 100644 --- a/.github/workflows/cd_publish.yml +++ b/.github/workflows/cd_publish.yml @@ -7,7 +7,7 @@ on: jobs: publish: name: External - uses: SINTEF/ci-cd/.github/workflows/cd_release.yml@v2.5.1 + uses: SINTEF/ci-cd/.github/workflows/cd_release.yml@v2.5.2 if: github.repository == 'emmo-repo/EMMOntoPy' && startsWith(github.ref, 'refs/tags/v') with: git_username: EMMOntoPy Developers @@ -16,7 +16,7 @@ jobs: # Publish package python_package: true - python_version_build: "3.7" + python_version_build: "3.9" # We're mentioning only 'ontopy', since the version is set statically only in # ontopy/__init__.py package_dirs: ontopy @@ -26,7 +26,7 @@ jobs: # Update documentation update_docs: true - python_version_docs: "3.7" + python_version_docs: "3.9" doc_extras: "[docs]" changelog_exclude_labels: dependencies diff --git a/.github/workflows/ci_automerge_dependabot.yml b/.github/workflows/ci_automerge_dependabot.yml index 7e99c13aa..661b57d75 100644 --- a/.github/workflows/ci_automerge_dependabot.yml +++ b/.github/workflows/ci_automerge_dependabot.yml @@ -7,7 +7,7 @@ on: jobs: update-dependabot-branch: name: External - uses: SINTEF/ci-cd/.github/workflows/ci_automerge_prs.yml@v2.5.1 + uses: SINTEF/ci-cd/.github/workflows/ci_automerge_prs.yml@v2.5.2 if: github.repository_owner == 'emmo-repo' && startsWith(github.event.pull_request.head.ref, 'dependabot/') && github.actor == 'dependabot[bot]' secrets: PAT: ${{ secrets.RELEASE_PAT }} diff --git a/.github/workflows/ci_cd_updated_master.yml b/.github/workflows/ci_cd_updated_master.yml index 0bc7384a9..92a652452 100644 --- a/.github/workflows/ci_cd_updated_master.yml +++ b/.github/workflows/ci_cd_updated_master.yml @@ -7,7 +7,7 @@ on: jobs: updates-to-master: name: External - uses: SINTEF/ci-cd/.github/workflows/ci_cd_updated_default_branch.yml@v2.5.1 + uses: SINTEF/ci-cd/.github/workflows/ci_cd_updated_default_branch.yml@v2.5.2 if: github.repository_owner == 'emmo-repo' with: git_username: EMMOntoPy Developers @@ -22,7 +22,7 @@ jobs: package_dirs: | emmopy ontopy - python_version: "3.7" + python_version: "3.9" doc_extras: "[docs]" special_file_api_ref_options: "emmopy/emmocheck.py,show_bases: false" landing_page_replacements: | diff --git a/.github/workflows/ci_dependabot.yml b/.github/workflows/ci_dependabot.yml index e4a01eb1b..6e4e31ad4 100644 --- a/.github/workflows/ci_dependabot.yml +++ b/.github/workflows/ci_dependabot.yml @@ -11,7 +11,7 @@ on: jobs: create-collected-pr: name: External - uses: SINTEF/ci-cd/.github/workflows/ci_update_dependencies.yml@v2.5.1 + uses: SINTEF/ci-cd/.github/workflows/ci_update_dependencies.yml@v2.5.2 if: github.repository_owner == 'emmo-repo' with: git_username: EMMOntoPy Developers @@ -21,7 +21,7 @@ jobs: pr_labels: dependencies,github_actions extra_to_dos: "- [ ] Make sure that the PR is **squash** merged, with a sensible commit message." update_pre-commit: true - python_version: "3.8" + python_version: "3.9" install_extras: "[dev,docs]" skip_pre-commit_hooks: pylint secrets: diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index e8d0043f8..7396f190a 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -10,19 +10,19 @@ on: jobs: tests: name: External - uses: SINTEF/ci-cd/.github/workflows/ci_tests.yml@v2.5.1 + uses: SINTEF/ci-cd/.github/workflows/ci_tests.yml@v2.5.2 with: # General install_extras: "[dev,docs]" # pre-commit run_pre-commit: true - python_version_pre-commit: "3.8" + python_version_pre-commit: "3.9" skip_pre-commit_hooks: pylint # pylint run_pylint: true - python_version_pylint_safety: "3.7" + python_version_pylint_safety: "3.9" pylint_options: "--rcfile=pyproject.toml" pylint_targets: "*.py tools emmopy ontopy" @@ -42,13 +42,13 @@ jobs: # Build distribution run_build_package: true - python_version_package: "3.7" + python_version_package: "3.9" build_cmd: "python -m build" # Build documentation # Exclude base classes in emmopy.emmocheck run_build_docs: true - python_version_docs: "3.7" + python_version_docs: "3.9" update_python_api_ref: true update_docs_landing_page: true package_dirs: | @@ -71,7 +71,7 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v4 @@ -91,7 +91,7 @@ jobs: 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' + if: matrix.python-version == '3.9' && github.repository == 'emmo-repo/EMMOntoPy' uses: codecov/codecov-action@v3 # - name: Run vertical demo @@ -115,12 +115,12 @@ jobs: name: EMMO documentation (test using ontodoc) runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - name: Set up Python 3.7 + - name: Set up Python 3.9 uses: actions/setup-python@v4 with: - python-version: "3.7" + python-version: "3.9" - name: Check Ubuntu version we are running under run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5fa382884..1d3f634fa 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.4.0 + rev: v2.5.0 hooks: - id: docs-api-reference args: diff --git a/README.md b/README.md index c28466901..9fea7366f 100644 --- a/README.md +++ b/README.md @@ -3,45 +3,68 @@ # EMMOntoPy -*Python API for the Elemental Multiperspective Material Ontology ([EMMO]).* +*Library for representing and working with ontologies in Python.* ![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/).* - -This package is based on [Owlready2] and provides an intuitive representation of [EMMO] in Python. -It is available on [GitHub][EMMOntoPy] and on [PyPI][PyPI:EMMOntoPy] under the open source [BSD 3-Clause license](LICENSE.txt). - -The Elemental Multiperspective Material Ontology (EMMO) is an ongoing effort to create an ontology that takes into account fundamental concepts of physics, chemistry and materials science and is designed to pave the road for semantic interoperability. -The aim of EMMO is to be generic and provide a common ground for describing materials, models and data that can be adapted by all domains. - -EMMO is formulated using OWL. -EMMOntoPy is a Python API for using EMMO to solving real problems. -By using the excellent Python package [Owlready2], EMMOntoPy provides a natural representation of EMMO in Python. -On top of that EMMOntoPy provides: - -- Access by label (as well as by names, important since class and property names in EMMO are based on UUIDs). -- Test suite for EMMO-based ontologies. -- Generation of graphs. -- Generation of documentation. -- Command-line tools: - - [`emmocheck`](docs/tools-instructions.md#emmocheck): - Checks an ontology against EMMO conventions. - - [`ontoversion`](docs/tools-instructions.md#ontoversion): - Prints ontology version number. +EMMOntoPy is a Python package based on the excellent [Owlready2], which provides a natural and intuitive representation of ontologies in Python. +EMMOntoPy extends Owlready2 and adds additional functionality, like accessing entities by label, reasoning with FaCT++ and parsing logical expressions in Manchester syntax. +It also includes a set of tools, like creating an ontology from an Excel sheet, generation of reference documentation of ontologies and visualisation of ontologies graphically. +EMMOntoPy is freely available for on GitHub and on PyPI under the permissive open source [BSD 3-Clause license](LICENSE.txt). + +EMMOntoPy was originally developed to work effectively with the Elemental Multiperspective Material Ontology ([EMMO]) and EMMO-based domain ontologies. +It has now two sub-packages, `ontopy` and `emmopy`, where `ontopy` is a general package to work with any OWL ontology, while `emmopy` provides extra features that are specific to [EMMO]. + +Owlready2, and thereby also EMMOntoPy, represents OWL classes and individuals in Python as classes and instances. +OWL properties are represented as Python attributes. +Hence, it provides a new *dot* notation for representing ontologies as valid Python code. +The notation is simple and easy to understand and write for people with some knowledge of OWL and Python. +Since Python is a versatile programming language, Owlready2 does not only allow for representation of OWL ontologies, but also to work with them programmatically, including interpretation, modification and generation. +Some of the additional features provided by EMMOntoPy are are listed below: + +## Access by label +In Owlready2 ontological entities, like classes, properties and individuals are accessed by the name-part of their IRI (i.e. everything that follows after the final slash or hash in the IRI). +This is very inconvenient for ontologies like EMMO or Wikidata, that identify ontological entities by long numerical names. +For instance, the name-part of the IRI of the Atom class in EMMO is ‘EMMO_eb77076b_a104_42ac_a065_798b2d2809ad’, which is neither human readable nor easy to write. +EMMOntoPy allows to access the entity via its label (or rather skos:prefLabel) ‘Atom’, which is much more user friendly. + +## Turtle serialisation/deserialisation +The Terse RDF Triple Language (Turtle) is a common syntax and file format for representing ontologies. +EMMOntoPy adds support for reading and writing ontologies in turtle format. + +## FaCT++ reasoning +Owlready2 has only support for reasoning with HermiT and Pellet. +EMMOntoPy adds additional support for the fast tableaux-based [FaCT++ reasoner] for description logics. + +## Manchester syntax +Even though the Owlready2 dot notation is clear and easy to read and understand for people who know Python, it is a new syntax that may look foreign for people that are used to working with Protégé. +EMMOntoPy provides support to parse and serialise logical expressions in [Manchester syntax], making it possible to create tools that will be much more familiar to work with for people used to working with Protégé. + +## Visualisation +EMMOntoPy provides a Python module for graphical visualisation of ontologies. +This module allows to graphically represent not only the taxonomy, but also restrictions and logical constructs. +The classes to include in the graph, can either be specified manually or inferred from the taxonomy (like all subclasses of a give class that are not a subclass of any class in a set of other classes). + +## Tools +EMMOntoPy includes a small set of command-line tools implemented as Python scripts: + - [`ontoconvert`](docs/tools-instructions.md#ontoconvert): + Converts ontologies between different file formats. + It also supports some additional transformation during conversion, like running a reasoner, merging several ontological modules together (squashing), rename IRIs, generate catalogue file and automatic annotation of entities with their source IRI. - [`ontograph`](docs/tools-instructions.md#ontograph): - Vertasile tool for visualising (parts of) an ontology. + Vertasile tool for visualising (parts of) an ontology, utilising the visualisation features mention above. - [`ontodoc`](docs/tools-instructions.md#ontodoc): Documents an ontology. - - [`ontoconvert`](docs/tools-instructions.md#ontoconvert): - Converts between ontology formats. - [`excel2onto`](docs/tools-instructions.md#excel2onto): Generate an EMMO-based ontology from an excel file. + It is useful for domain experts with limited knowledge of ontologies and that are not used to tools like Protégé. + - [`ontoversion`](docs/tools-instructions.md#ontoversion): + Prints ontology version number. + - [`emmocheck`](docs/tools-instructions.md#emmocheck): + A small test framework for checking the consistency of EMMO and EMMO-based domain ontologies and whether they confirm to the EMMO conventions. -Some examples of what you can do with EMMOntoPy includes: +## Some examples of what you can do with EMMOntoPy includes: - Access and query RDF-based ontologies from your application. This includes several different flavors of RDF (OWL, **Turtle (`ttl`)**, and more). @@ -181,3 +204,5 @@ It has mainly been developed by [SINTEF](https://www.sintef.no/), specifically: [Pygments]: https://pypi.org/project/Pygments/ [semver]: https://pypi.org/project/semver/ [rdflib]: https://pypi.org/project/rdflib/ +[FaCT++]: http://owl.cs.manchester.ac.uk/tools/fact/ +[Manchester syntax]: https://www.w3.org/TR/owl2-manchester-syntax/ diff --git a/demo/horizontal/emmo2meta.py b/demo/horizontal/emmo2meta.py index 1c4538938..91753a914 100644 --- a/demo/horizontal/emmo2meta.py +++ b/demo/horizontal/emmo2meta.py @@ -393,7 +393,7 @@ def get_description(self, cls): """Returns description for OWL class `cls` by combining its annotations.""" if isinstance(cls, str): - cls = onto[cls] + cls = self.onto[cls] descr = [] annotations = self.onto.get_annotations(cls) if "definition" in annotations: diff --git a/docs/index.md b/docs/index.md index 2107265f1..1312a062e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -3,45 +3,68 @@ # EMMOntoPy -*Python API for the Elemental Multiperspective Material Ontology ([EMMO]).* +*Library for representing and working with ontologies in Python.* ![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/).* - -This package is based on [Owlready2] and provides an intuitive representation of [EMMO] in Python. -It is available on [GitHub][EMMOntoPy] and on [PyPI][PyPI:EMMOntoPy] under the open source [BSD 3-Clause license](LICENSE.md). - -The Elemental Multiperspective Material Ontology (EMMO) is an ongoing effort to create an ontology that takes into account fundamental concepts of physics, chemistry and materials science and is designed to pave the road for semantic interoperability. -The aim of EMMO is to be generic and provide a common ground for describing materials, models and data that can be adapted by all domains. - -EMMO is formulated using OWL. -EMMOntoPy is a Python API for using EMMO to solving real problems. -By using the excellent Python package [Owlready2], EMMOntoPy provides a natural representation of EMMO in Python. -On top of that EMMOntoPy provides: - -- Access by label (as well as by names, important since class and property names in EMMO are based on UUIDs). -- Test suite for EMMO-based ontologies. -- Generation of graphs. -- Generation of documentation. -- Command-line tools: - - [`emmocheck`](tools-instructions.md#emmocheck): - Checks an ontology against EMMO conventions. - - [`ontoversion`](tools-instructions.md#ontoversion): - Prints ontology version number. +EMMOntoPy is a Python package based on the excellent [Owlready2], which provides a natural and intuitive representation of ontologies in Python. +EMMOntoPy extends Owlready2 and adds additional functionality, like accessing entities by label, reasoning with FaCT++ and parsing logical expressions in Manchester syntax. +It also includes a set of tools, like creating an ontology from an Excel sheet, generation of reference documentation of ontologies and visualisation of ontologies graphically. +EMMOntoPy is freely available for on GitHub and on PyPI under the permissive open source [BSD 3-Clause license](LICENSE.md). + +EMMOntoPy was originally developed to work effectively with the Elemental Multiperspective Material Ontology ([EMMO]) and EMMO-based domain ontologies. +It has now two sub-packages, `ontopy` and `emmopy`, where `ontopy` is a general package to work with any OWL ontology, while `emmopy` provides extra features that are specific to [EMMO]. + +Owlready2, and thereby also EMMOntoPy, represents OWL classes and individuals in Python as classes and instances. +OWL properties are represented as Python attributes. +Hence, it provides a new *dot* notation for representing ontologies as valid Python code. +The notation is simple and easy to understand and write for people with some knowledge of OWL and Python. +Since Python is a versatile programming language, Owlready2 does not only allow for representation of OWL ontologies, but also to work with them programmatically, including interpretation, modification and generation. +Some of the additional features provided by EMMOntoPy are are listed below: + +## Access by label +In Owlready2 ontological entities, like classes, properties and individuals are accessed by the name-part of their IRI (i.e. everything that follows after the final slash or hash in the IRI). +This is very inconvenient for ontologies like EMMO or Wikidata, that identify ontological entities by long numerical names. +For instance, the name-part of the IRI of the Atom class in EMMO is ‘EMMO_eb77076b_a104_42ac_a065_798b2d2809ad’, which is neither human readable nor easy to write. +EMMOntoPy allows to access the entity via its label (or rather skos:prefLabel) ‘Atom’, which is much more user friendly. + +## Turtle serialisation/deserialisation +The Terse RDF Triple Language (Turtle) is a common syntax and file format for representing ontologies. +EMMOntoPy adds support for reading and writing ontologies in turtle format. + +## FaCT++ reasoning +Owlready2 has only support for reasoning with HermiT and Pellet. +EMMOntoPy adds additional support for the fast tableaux-based [FaCT++ reasoner] for description logics. + +## Manchester syntax +Even though the Owlready2 dot notation is clear and easy to read and understand for people who know Python, it is a new syntax that may look foreign for people that are used to working with Protégé. +EMMOntoPy provides support to parse and serialise logical expressions in [Manchester syntax], making it possible to create tools that will be much more familiar to work with for people used to working with Protégé. + +## Visualisation +EMMOntoPy provides a Python module for graphical visualisation of ontologies. +This module allows to graphically represent not only the taxonomy, but also restrictions and logical constructs. +The classes to include in the graph, can either be specified manually or inferred from the taxonomy (like all subclasses of a give class that are not a subclass of any class in a set of other classes). + +## Tools +EMMOntoPy includes a small set of command-line tools implemented as Python scripts: + - [`ontoconvert`](tools-instructions.md#ontoconvert): + Converts ontologies between different file formats. + It also supports some additional transformation during conversion, like running a reasoner, merging several ontological modules together (squashing), rename IRIs, generate catalogue file and automatic annotation of entities with their source IRI. - [`ontograph`](tools-instructions.md#ontograph): - Vertasile tool for visualising (parts of) an ontology. + Vertasile tool for visualising (parts of) an ontology, utilising the visualisation features mention above. - [`ontodoc`](tools-instructions.md#ontodoc): Documents an ontology. - - [`ontoconvert`](tools-instructions.md#ontoconvert): - Converts between ontology formats. - [`excel2onto`](tools-instructions.md#excel2onto): Generate an EMMO-based ontology from an excel file. + It is useful for domain experts with limited knowledge of ontologies and that are not used to tools like Protégé. + - [`ontoversion`](tools-instructions.md#ontoversion): + Prints ontology version number. + - [`emmocheck`](tools-instructions.md#emmocheck): + A small test framework for checking the consistency of EMMO and EMMO-based domain ontologies and whether they confirm to the EMMO conventions. -Some examples of what you can do with EMMOntoPy includes: +## Some examples of what you can do with EMMOntoPy includes: - Access and query RDF-based ontologies from your application. This includes several different flavors of RDF (OWL, **Turtle (`ttl`)**, and more). @@ -181,3 +204,5 @@ It has mainly been developed by [SINTEF](https://www.sintef.no/), specifically: [Pygments]: https://pypi.org/project/Pygments/ [semver]: https://pypi.org/project/semver/ [rdflib]: https://pypi.org/project/rdflib/ +[FaCT++]: http://owl.cs.manchester.ac.uk/tools/fact/ +[Manchester syntax]: https://www.w3.org/TR/owl2-manchester-syntax/ diff --git a/emmopy/emmocheck.py b/emmopy/emmocheck.py index b90325667..f81dcab20 100644 --- a/emmopy/emmocheck.py +++ b/emmopy/emmocheck.py @@ -119,6 +119,7 @@ def test_class_label(self): "2-manifold", "3-manifold", "C++", + "3DPrinting", ) ) exceptions.update(self.get_config("test_class_label.exceptions", ())) @@ -236,7 +237,7 @@ def test_unit_dimension(self): msg=cls, ) - def test_quantity_dimension(self): + def test_quantity_dimension_beta3(self): """Check that all quantities have a physicalDimension annotation. Note: this test will be deprecated when isq is moved to emmo/domain. @@ -310,6 +311,113 @@ def test_quantity_dimension(self): physdim = anno["physicalDimension"].first() self.assertRegex(physdim, regex, msg=cls) + def test_quantity_dimension(self): + """Check that all quantities have a physicalDimension. + + Note: this test will be deprecated when isq is moved to emmo/domain. + + Configurations: + exceptions - full class names of classes to ignore. + """ + # pylint: disable=invalid-name + exceptions = set( + ( + "properties.ModelledQuantitativeProperty", + "properties.MeasuredQuantitativeProperty", + "properties.ConventionalQuantitativeProperty", + "metrology.QuantitativeProperty", + "metrology.Quantity", + "metrology.OrdinalQuantity", + "metrology.BaseQuantity", + "metrology.PhysicalConstant", + "metrology.PhysicalQuantity", + "metrology.ExactConstant", + "metrology.MeasuredConstant", + "metrology.DerivedQuantity", + "isq.ISQBaseQuantity", + "isq.InternationalSystemOfQuantity", + "isq.ISQDerivedQuantity", + "isq.SIExactConstant", + "emmo.ModelledQuantitativeProperty", + "emmo.MeasuredQuantitativeProperty", + "emmo.ConventionalQuantitativeProperty", + "emmo.QuantitativeProperty", + "emmo.Quantity", + "emmo.OrdinalQuantity", + "emmo.BaseQuantity", + "emmo.PhysicalConstant", + "emmo.PhysicalQuantity", + "emmo.ExactConstant", + "emmo.MeasuredConstant", + "emmo.DerivedQuantity", + "emmo.ISQBaseQuantity", + "emmo.InternationalSystemOfQuantity", + "emmo.ISQDerivedQuantity", + "emmo.SIExactConstant", + "emmo.NonSIUnits", + "emmo.StandardizedPhysicalQuantity", + "emmo.CategorizedPhysicalQuantity", + "emmo.ISO80000Categorised", + "emmo.AtomicAndNuclear", + "emmo.Defined", + "emmo.Electromagnetic", + "emmo.FrequentlyUsed", + "emmo.ChemicalCompositionQuantity", + "emmo.EquilibriumConstant", # physical dimension may change + "emmo.Solubility", + "emmo.Universal", + "emmo.Intensive", + "emmo.Extensive", + "emmo.Concentration", + ) + ) + if not hasattr(self.onto, "PhysicalQuantity"): + return + exceptions.update( + self.get_config("test_quantity_dimension.exceptions", ()) + ) + classes = set(self.onto.classes(self.check_imported)) + for cls in self.onto.PhysicalQuantity.descendants(): + if not self.check_imported and cls not in classes: + continue + if issubclass(cls, self.onto.ISO80000Categorised): + continue + if repr(cls) not in exceptions: + with self.subTest(cls=cls, label=get_label(cls)): + for r in cls.get_indirect_is_a(): + if isinstance(r, owlready2.Restriction) and repr( + r + ).startswith("emmo.hasMeasurementUnit.some"): + self.assertTrue( + issubclass( + r.value, + ( + self.onto.DimensionalUnit, + self.onto.DimensionlessUnit, + ), + ) + ) + break + else: + self.assertTrue( + issubclass(cls, self.onto.ISQDimensionlessQuantity) + ) + + def test_dimensional_unit(self): + """Check correct syntax of dimension string of dimensional units.""" + # pylint: disable=invalid-name + regex = re.compile( + "^T([+-][1-9][0-9]*|0) L([+-][1-9]|0) M([+-][1-9]|0) " + "I([+-][1-9]|0) (H|Θ)([+-][1-9]|0) N([+-][1-9]|0) " + "J([+-][1-9]|0)$" + ) + for cls in self.onto.SIDimensionalUnit.__subclasses__(): + with self.subTest(cls=cls, label=get_label(cls)): + self.assertEqual(len(cls.equivalent_to), 1) + r = cls.equivalent_to[0] + self.assertIsInstance(r, owlready2.Restriction) + self.assertRegex(r.value, regex) + def test_physical_quantity_dimension(self): """Check that all physical quantities have `hasPhysicalDimension`. @@ -660,6 +768,8 @@ def main( skipped = set( # skipped by default [ "test_namespace", + "test_physical_quantity_dimension_annotation", + "test_quantity_dimension_beta3", "test_physical_quantity_dimension", ] ) diff --git a/ontopy/excelparser.py b/ontopy/excelparser.py index 90852a57f..eff2f5657 100755 --- a/ontopy/excelparser.py +++ b/ontopy/excelparser.py @@ -319,12 +319,10 @@ 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 - - onto.sync_python_names() + # onto.sync_python_names() # prefLabel, label, and altLabel # are default label annotations onto.set_default_label_annotations() - # Add object properties if objectproperties is not None: objectproperties = _clean_dataframe(objectproperties) @@ -367,7 +365,6 @@ def create_ontology_from_pandas( # pylint:disable=too-many-locals,too-many-bran onto.sync_attributes( name_policy="uuid", name_prefix="EMMO_", class_docstring="elucidation" ) - # Clean up data frame with new concepts data = _clean_dataframe(data) # Add entities @@ -562,7 +559,6 @@ def get_metadata_from_dataframe( # pylint: disable=too-many-locals,too-many-bra ) except AttributeError: pass - return onto, catalog diff --git a/ontopy/graph.py b/ontopy/graph.py index 0ad9e578a..7b7ac7f05 100644 --- a/ontopy/graph.py +++ b/ontopy/graph.py @@ -720,11 +720,11 @@ def _relation_styles( """ for relation in entity.mro(): if relation in rels: - if get_label(relation) in relations: - rattrs = relations[get_label(relation)] + if str(get_label(relation)) in relations: + rattrs = relations[str(get_label(relation))] else: for alt_label in relation.get_annotations()["altLabel"]: - rattrs = relations[alt_label] + rattrs = relations[str(alt_label)] break else: diff --git a/ontopy/ontology.py b/ontopy/ontology.py index 851f6ae23..1b910ba94 100644 --- a/ontopy/ontology.py +++ b/ontopy/ontology.py @@ -49,7 +49,7 @@ ) if TYPE_CHECKING: - from typing import List + from typing import List, Sequence # Default annotations to look up @@ -76,18 +76,33 @@ def __init__(self, *args, **kwargs): self._iri_mappings = {} # all iri mappings loaded so far super().__init__(*args, **kwargs) - def get_ontology(self, base_iri="emmo-inferred"): + def get_ontology( + self, + base_iri: str = "emmo-inferred", + OntologyClass: "owlready2.Ontology" = None, + label_annotations: "Sequence" = None, + ) -> "Ontology": + # pylint: disable=too-many-branches """Returns a new Ontology from `base_iri`. - The `base_iri` argument may be one of: - - valid URL (possible excluding final .owl or .ttl) - - file name (possible excluding final .owl or .ttl) - - "emmo": load latest version of asserted EMMO - - "emmo-inferred": load latest version of inferred EMMO - (default) - - "emmo-development": load latest inferred development version - of EMMO. Until first stable release emmo-inferred and - emmo-development will be the same. + Arguments: + base_iri: The base IRI of the ontology. May be one of: + - valid URL (possible excluding final .owl or .ttl) + - file name (possible excluding final .owl or .ttl) + - "emmo": load latest version of asserted EMMO + - "emmo-inferred": load latest version of inferred EMMO + (default) + - "emmo-development": load latest inferred development + version of EMMO. Until first stable release + emmo-inferred and emmo-development will be the same. + OntologyClass: If given and `base_iri` doesn't correspond + to an existing ontology, a new ontology is created of + this Ontology subclass. Defaults to `ontopy.Ontology`. + label_annotations: Sequence of label IRIs used for accessing + entities in the ontology given that they are in the ontology. + Label IRIs not in the ontology will need to be added to + ontologies in order to be accessible. + Defaults to DEFAULT_LABEL_ANNOTATIONS if set to None. """ base_iri = base_iri.as_uri() if isinstance(base_iri, Path) else base_iri @@ -124,7 +139,14 @@ def get_ontology(self, base_iri="emmo-inferred"): if iri[-1] not in "/#": iri += "#" - onto = Ontology(self, iri) + + if OntologyClass is None: + OntologyClass = Ontology + + onto = OntologyClass(self, iri) + + if label_annotations: + onto.label_annotations = list(label_annotations) return onto @@ -148,19 +170,10 @@ class Ontology(owlready2.Ontology): # pylint: disable=too-many-public-methods """A generic class extending owlready2.Ontology.""" def __init__(self, *args, **kwargs): - # Properties controlling what annotations that are considered by - # get_by_label() super().__init__(*args, **kwargs) - self._label_annotations = None + self.label_annotations = DEFAULT_LABEL_ANNOTATIONS[:] self.prefix = None - # Properties controlling what annotations that are considered by - # get_by_label() - label_annotations = property( - fget=lambda self: self._label_annotations, - doc="List of label annotation searched for by get_by_label().", - ) - # Name of special unlabeled entities, like Thing, Nothing, etc... _special_labels = None @@ -205,13 +218,13 @@ def __dir__(self): lst = list(self.get_entities(imported=self._dir_imported)) if self._dir_preflabel: dirset.update( - dir.prefLabel.first() + str(dir.prefLabel.first()) for dir in lst if hasattr(dir, "prefLabel") ) if self._dir_label: dirset.update( - dir.label.first() for dir in lst if hasattr(dir, "label") + str(dir.label.first()) for dir in lst if hasattr(dir, "label") ) if self._dir_name: dirset.update(dir.name for dir in lst if hasattr(dir, "name")) @@ -275,12 +288,13 @@ def get_unabbreviated_triples( def set_default_label_annotations(self): """Sets the default label annotations.""" - if self._label_annotations is None: - for iri in DEFAULT_LABEL_ANNOTATIONS: - try: - self.add_label_annotation(iri) - except ValueError: - pass + warnings.warn( + "Ontology.set_default_label_annotations() is deprecated. " + "Default label annotations are set by Ontology.__init__(). ", + DeprecationWarning, + stacklevel=2, + ) + self.label_annotations = DEFAULT_LABEL_ANNOTATIONS[:] def get_by_label( self, @@ -292,8 +306,8 @@ def get_by_label( ): """Returns entity with label annotation `label`. - Args: - label: label so serach for. + Arguments: + label: label so search for. May be written as 'label' or 'prefix:label'. get_by_label('prefix:label') == get_by_label('label', prefix='prefix'). @@ -320,9 +334,12 @@ def get_by_label( # pylint: disable=too-many-arguments,too-many-branches,invalid-name if not isinstance(label, str): raise TypeError( - f"Invalid label definition, must be a string: {label!r}" + f"Invalid label definition, must be a string: '{label}'" ) + if label_annotations is None: + label_annotations = self.label_annotations + if colon_in_label is None: colon_in_label = self._colon_in_label if colon_in_label: @@ -353,12 +370,12 @@ def get_by_label( return entityset.pop() if len(entityset) > 1: raise AmbiguousLabelError( - f"Several entities have the same label {label!r} " - f"with prefix {prefix!r}." + f"Several entities have the same label '{label}' " + f"with prefix '{prefix}'." ) raise NoSuchLabelError( - f"No label annotations matches for {label!r} " - f"with prefix {prefix!r}." + f"No label annotations matches for '{label}' " + f"with prefix '{prefix}'." ) # Label is a full IRI @@ -366,26 +383,15 @@ def get_by_label( if entity: return entity - # First entity with matching label annotation - - if label_annotations: - annotation_ids = ( - self._abbreviate(ann, False) for ann in label_annotations - ) - elif self._label_annotations: - annotation_ids = (ann.storid for ann in self._label_annotations) - else: - annotation_ids = None - get_triples = ( self.world._get_data_triples_spod_spod if imported else self._get_data_triples_spod_spod ) - if annotation_ids: - for annotation_id in annotation_ids: - for s, _, _, _ in get_triples(None, annotation_id, label, None): - return self.world[self._unabbreviate(s)] + + for storid in self._to_storids(label_annotations): + for s, _, _, _ in get_triples(None, storid, label, None): + return self.world[self._unabbreviate(s)] # Special labels if self._special_labels and label in self._special_labels: @@ -396,25 +402,41 @@ def get_by_label( if entity: return entity - # Check if label is a name in any namespace - for namespace in self._namespaces.keys(): - entity = self.world[namespace + label] - if entity: + # Check label is the name of an entity + for entity in self.get_entities(imported=imported): + if label == entity.name: return entity raise NoSuchLabelError(f"No label annotations matches '{label}'") - def get_by_label_all(self, label, label_annotations=None, prefix=None): - """Like get_by_label(), but returns a list with all matching labels. - - Returns an empty list if no matches could be found. + def get_by_label_all( + self, + label, + label_annotations=None, + prefix=None, + exact_match=False, + ) -> "Set[Optional[owlready2.entity.EntityClass]]": + """Returns set of entities with label annotation `label`. + + Arguments: + label: label so search for. + May be written as 'label' or 'prefix:label'. Wildcard matching + using glob pattern is also supported if `exact_match` is set to + false. + label_annotations: a sequence of label annotation names to look up. + Defaults to the `label_annotations` property. + prefix: if provided, it should be the last component of + the base iri of an ontology (with trailing slash (/) or hash + (#) stripped off). The search for a matching label will be + limited to this namespace. + exact_match: Do not treat "*" and brackets as special characters + when matching. May be useful if your ontology has labels + containing such labels. - Note - ---- - The current implementation also supports "*" as a wildcard - matching any number of characters. This may change in the future. + Returns: + Set of all matching entities or an empty set if no matches + could be found. """ - if not isinstance(label, str): raise TypeError( f"Invalid label definition, " f"must be a string: {label!r}" @@ -424,32 +446,45 @@ def get_by_label_all(self, label, label_annotations=None, prefix=None): f"Invalid label definition, {label!r} contains spaces." ) - if label_annotations: - annotations = ( - ann.name if hasattr(ann, "storid") else ann - for ann in label_annotations - ) - elif self._label_annotations: - annotations = (ann.name for ann in self.label_annotations) + if label_annotations is None: + label_annotations = self.label_annotations - else: - annotations = None entities = set() - if annotations: - for key in annotations: + + # Check label annotations + if exact_match: + for storid in self._to_storids(label_annotations): + entities.update( + self.world._get_by_storid(s) + for s, _, _ in self.world._get_data_triples_spod_spod( + None, storid, str(label), None + ) + ) + else: + for storid in self._to_storids(label_annotations): + label_entity = self._unabbreviate(storid) + key = ( + label_entity.name + if hasattr(label_entity, "name") + else label_entity + ) entities.update(self.world.search(**{key: label})) if self._special_labels and label in self._special_labels: entities.update(self._special_labels[label]) - # Find existence in get_entities - matches = fnmatch.filter( - (ent.name for ent in self.get_entities()), label - ) - - entities.update( - ent for ent in self.get_entities() if ent.name in matches - ) + # Check name-part of IRI + if exact_match: + entities.update( + ent for ent in self.get_entities() if ent.name == str(label) + ) + else: + matches = fnmatch.filter( + (ent.name for ent in self.get_entities()), label + ) + entities.update( + ent for ent in self.get_entities() if ent.name in matches + ) if prefix: return set( @@ -459,32 +494,57 @@ def get_by_label_all(self, label, label_annotations=None, prefix=None): ) return entities - def add_label_annotation(self, iri): - """Adds label annotation used by get_by_label(). + def _to_storids(self, sequence, create_if_missing=False): + """Return a list of storid's corresponding to the elements in the + sequence `sequence`. + + The elements may be either be full IRIs (strings) or Owlready2 + entities with an associated storid. - May be provided either as an IRI or as its owlready2 representation. + If `create_if_missing` is true, new Owlready2 entities will be + created for IRIs that not already are associated with an + entity. Otherwise such IRIs will be skipped in the returned + list. """ - if self._label_annotations is None: - self._label_annotations = [] - label_annotation = iri if hasattr(iri, "storid") else self.world[iri] - if label_annotation is None: - warnings.warn(f"adding new IRI to ontology: {iri}") - name = iri.rsplit("/")[-1].rsplit("#")[-1] - bases = (owlready2.AnnotationProperty,) - with self: - label_annotation = types.new_class(name, bases) - if label_annotation not in self._label_annotations: - self._label_annotations.append(label_annotation) + if not sequence: + return [] + storids = [] + for element in sequence: + if hasattr(element, "storid"): + storids.append(element.storid) + else: + storid = self.world._abbreviate(element, create_if_missing) + if storid: + storids.append(storid) + return storids - def remove_label_annotation(self, iri): - """Removes label annotation used by get_by_label(). + def add_label_annotation(self, iri): + """Adds label annotation used by get_by_label().""" + warnings.warn( + "Ontology.add_label_annotations() is deprecated. " + "Direct modify the `label_annotations` attribute instead.", + DeprecationWarning, + stacklevel=2, + ) + if hasattr(iri, "iri"): + iri = iri.iri + if iri not in self.label_annotations: + self.label_annotations.append(iri) - May be provided either as an IRI or as its owlready2 representation. - """ - label_annotation = iri if hasattr(iri, "storid") else self.world[iri] - if not label_annotation: - raise ValueError(f"IRI not in ontology: {iri}") - self._label_annotations.remove(label_annotation) + def remove_label_annotation(self, iri): + """Removes label annotation used by get_by_label().""" + warnings.warn( + "Ontology.remove_label_annotations() is deprecated. " + "Direct modify the `label_annotations` attribute instead.", + DeprecationWarning, + stacklevel=2, + ) + if hasattr(iri, "iri"): + iri = iri.iri + try: + self.label_annotations.remove(iri) + except ValueError: + pass def set_common_prefix( self, @@ -520,8 +580,8 @@ def load( # pylint: disable=too-many-arguments,arguments-renamed ): """Load the ontology. - Parameters - ---------- + Arguments + --------- only_local: bool Whether to only read local files. This requires that you have appended the path to the ontology to owlready2.onto_path. @@ -571,8 +631,6 @@ def load( # pylint: disable=too-many-arguments,arguments-renamed # Enable optimised search by get_by_label() if self._special_labels is None and emmo_based: - for iri in DEFAULT_LABEL_ANNOTATIONS: - self.add_label_annotation(iri) top = self.world["http://www.w3.org/2002/07/owl#topObjectProperty"] self._special_labels = { "Thing": owlready2.Thing, @@ -846,7 +904,6 @@ def save( ) revmap = {value: key for key, value in FMAP.items()} - if filename is None: if format: fmt = revmap.get(format, format) @@ -1004,51 +1061,100 @@ def get_entities( # pylint: disable=too-many-arguments def classes(self, imported=False): """Returns an generator over all classes. - If `imported` is `True`, will imported classes are also returned. + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. """ + return self._entities("classes", imported=imported) + + def _entities( + self, entity_type, imported=False + ): # pylint: disable=too-many-branches + """Returns an generator over all entities of the desired type. + This is a helper function for `classes()`, `individuals()`, + `object_properties()`, `data_properties()` and + `annotation_properties()`. + + Arguments: + entity_type: The type of entity desired given as a string. + Can be any of `classes`, `individuals`, + `object_properties`, `data_properties` and + `annotation_properties`. + imported: if `True`, entities in imported ontologies + are also returned. + """ + + generator = [] if imported: - return self.world.classes() - return super().classes() + ontologies = self.get_imported_ontologies(recursive=True) + ontologies.append(self) + for onto in ontologies: + if entity_type == "classes": + for cls in list(onto.classes()): + generator.append(cls) + elif entity_type == "individuals": + for ind in list(onto.individuals()): + generator.append(ind) + elif entity_type == "object_properties": + for prop in list(onto.object_properties()): + generator.append(prop) + elif entity_type == "data_properties": + for prop in list(onto.data_properties()): + generator.append(prop) + elif entity_type == "annotation_properties": + for prop in list(onto.annotation_properties()): + generator.append(prop) + else: + if entity_type == "classes": + generator = super().classes() + elif entity_type == "individuals": + generator = super().individuals() + elif entity_type == "object_properties": + generator = super().object_properties() + elif entity_type == "data_properties": + generator = super().data_properties() + elif entity_type == "annotation_properties": + generator = super().annotation_properties() + + for entity in generator: + yield entity def individuals(self, imported=False): """Returns an generator over all individuals. - If `imported` is `True`, will imported individuals are also returned. + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. """ - if imported: - return self.world.individuals() - return super().individuals() + return self._entities("individuals", imported=imported) def object_properties(self, imported=False): - """Returns an generator over all object properties. + """Returns an generator over all object_properties. - If `imported` is true, will imported object properties are also - returned. + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. """ - if imported: - return self.world.object_properties() - return super().object_properties() + return self._entities("object_properties", imported=imported) def data_properties(self, imported=False): - """Returns an generator over all data properties. + """Returns an generator over all data_properties. - If `imported` is true, will imported data properties are also - returned. + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. """ - if imported: - return self.world.data_properties() - return super().data_properties() + return self._entities("data_properties", imported=imported) def annotation_properties(self, imported=False): - """Returns a generator iterating over all annotation properties - defined in the current ontology. + """Returns an generator over all annotation_properties. + + Arguments: + imported: if `True`, entities in imported ontologies + are also returned. - If `imported` is true, annotation properties in imported ontologies - will also be included. """ - if imported: - return self.world.annotation_properties() - return super().annotation_properties() + return self._entities("annotation_properties", imported=imported) def get_root_classes(self, imported=False): """Returns a list or root classes.""" @@ -1144,8 +1250,8 @@ def sync_reasoner( sync = owlready2.sync_reasoner_hermit else: raise ValueError( - f"unknown reasoner {reasoner!r}. Supported reasoners " - 'are "Pellet", "HermiT" and "FaCT++".' + f"unknown reasoner '{reasoner}'. Supported reasoners " + "are 'Pellet', 'HermiT' and 'FaCT++'." ) # For some reason we must visit all entities once before running @@ -1265,6 +1371,7 @@ def get_relations(self): "Ontology.get_relations() is deprecated. Use " "onto.object_properties() instead.", DeprecationWarning, + stacklevel=2, ) return self.object_properties() @@ -1275,6 +1382,7 @@ def get_annotations(self, entity): "Ontology.get_annotations(entity) is deprecated. Use " "entity.get_annotations() instead.", DeprecationWarning, + stacklevel=2, ) if isinstance(entity, str): @@ -1683,6 +1791,7 @@ def new_entity( AnnotationPropertyClass, ] ] = "class", + preflabel: Optional[str] = None, ) -> Union[ ThingClass, ObjectPropertyClass, @@ -1701,6 +1810,11 @@ def new_entity( 'annotation_property' (strings) or the Python classes ObjectPropertyClass, DataPropertyClass and AnnotationProperty classes. + preflabel: if given, add this as a skos:prefLabel annotation + to the new entity. If None (default), `name` will + be added as prefLabel if skos:prefLabel is in the ontology + and listed in `self.label_annotations`. Set `preflabel` to + False, to avoid assigning a prefLabel. Returns: the new entity. @@ -1710,13 +1824,13 @@ def new_entity( By default, the parent is Thing. """ + # pylint: disable=invalid-name if " " in name: raise LabelDefinitionError( f"Error in label name definition '{name}': " 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": @@ -1746,12 +1860,23 @@ def new_entity( with self: entity = types.new_class(name, parents) - # Set prefLabel to name if label_annotations is set - # and prefLabel is one of the annotations - if self.label_annotations and "prefLabel" in [ - ann.name for ann in self.label_annotations - ]: + + preflabel_iri = "http://www.w3.org/2004/02/skos/core#prefLabel" + if preflabel: + if not self.world[preflabel_iri]: + pref_label = self.new_annotation_property( + "prefLabel", + parent=[owlready2.AnnotationProperty], + ) + pref_label.iri = preflabel_iri + entity.prefLabel = english(preflabel) + elif ( + preflabel is None + and preflabel_iri in self.label_annotations + and self.world[preflabel_iri] + ): entity.prefLabel = english(name) + return entity # Method that creates new ThingClass using new_entity diff --git a/ontopy/patch.py b/ontopy/patch.py index 05235d9e3..23e28afb2 100644 --- a/ontopy/patch.py +++ b/ontopy/patch.py @@ -6,6 +6,7 @@ from owlready2 import AnnotationPropertyClass, ThingClass, PropertyClass from owlready2 import Metadata, Thing, Restriction, Namespace from ontopy.utils import EMMOntoPyException +from ontopy.ontology import Ontology as OntopyOntology def render_func(entity): @@ -27,6 +28,10 @@ def render_func(entity): # # Extending ThingClass (classes) # ============================== + +save_getattr = ThingClass.__getattr__ + + def get_preferred_label(self): """Returns the preferred label as a string (not list). @@ -69,7 +74,7 @@ def get_parents(self, strict=False): def _dir(self): """Extend dir() listing of ontology classes.""" set_dir = set(object.__dir__(self)) - props = self.namespace.world._props.keys() + props = [str(key) for key in self.namespace.world._props.keys()] set_dir.update(props) return sorted(set_dir) @@ -91,14 +96,16 @@ def _setitem(self, name, value): Example: >>> from emmopy import get_emmo + >>> from owlready2 import locstr >>> emmo = get_emmo() >>> emmo.Atom['altLabel'] - ['ChemicalElement'] + [locstr('ChemicalElement', 'en')] >>> emmo.Atom['altLabel'] = 'Element' + >>> emmo.Atom['altLabel'] = locstr('Atomo', 'it') >>> emmo.Atom['altLabel'] - ['ChemicalElement', 'Element'] - + [locstr('ChemicalElement', 'en'), 'Element', locstr('Atomo', 'it')] """ + item = _getitem(self, name) item.append(value) @@ -112,6 +119,24 @@ def _delitem(self, name): item.clear() +def _getattr(self, name): + """Provide attribute access to annotation properties. + + This upates __getattr__ in owlready2. If name is not found as + attribute it tries using the iriname of the annotation property. + """ + try: + return save_getattr(self, name) + except AttributeError as err: + # make sure we are using and ontopy Ontology which has get_by_label + if isinstance(self.namespace.ontology, OntopyOntology): + entity = self.namespace.ontology.get_by_label(name) + # add annotation property to world._props for faster access later + self.namespace.world._props[name] = entity + return save_getattr(self, entity.name) + raise err + + def get_annotations( self, all=False, imported=True ): # pylint: disable=redefined-builtin @@ -123,8 +148,9 @@ def get_annotations( ontologies. """ onto = self.namespace.ontology + annotations = { - get_preferred_label(_): _._get_values_for_class(self) + str(get_preferred_label(_)): _._get_values_for_class(self) for _ in onto.annotation_properties(imported=imported) } if all: @@ -196,6 +222,7 @@ def get_indirect_is_a(self, skip_classes=True): setattr(ThingClass, "__getitem__", _getitem) setattr(ThingClass, "__setitem__", _setitem) setattr(ThingClass, "__delitem__", _delitem) +setattr(ThingClass, "__getattr__", _getattr) setattr(ThingClass, "get_preferred_label", get_preferred_label) setattr(ThingClass, "get_parents", get_parents) setattr(ThingClass, "get_annotations", get_annotations) @@ -251,7 +278,7 @@ def namespace_init(self, world_or_ontology, base_iri, name=None): # Extending Metadata # ================== def keys(self): - """Return a generator over annotation property names associates + """Return a generator over annotation property names associated with this ontology.""" namespace = self.namespace for annotation in namespace.annotation_properties(): diff --git a/requirements.txt b/requirements.txt index 585212ba5..275fd0ba3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,7 +4,7 @@ defusedxml>=0.7.1,<1 graphviz>=0.16,<0.21 numpy>=1.19.5,<2 openpyxl>=3.0.9,<3.2 -Owlready2>=0.28,!=0.32,!=0.34,<0.42 +Owlready2>=0.28,!=0.32,!=0.34,<0.44 packaging>=21.0,<24 pandas>=1.2,<2.2 Pygments>=2.7.4,<3 diff --git a/requirements_dev.txt b/requirements_dev.txt index 9e0e64ed4..188c04c0b 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,6 @@ pre-commit>=2.21.0,<3; python_version<"3.8" pre-commit~=3.4; python_version>="3.8" -pylint~=2.17 +pylint~=2.17; python_version<"3.8" +pylint~=3.0; python_version>="3.8" pytest~=7.4 pytest-cov~=4.1 diff --git a/requirements_docs.txt b/requirements_docs.txt index f01e25c06..cce34520d 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,5 +1,8 @@ mike~=1.1 mkdocs~=1.5 mkdocs-awesome-pages-plugin~=2.9 -mkdocs-material~=9.2 mkdocstrings[python-legacy]~=0.23.0 +mkdocs-material~=9.4; python_version>="3.8" +mkdocs-material~=9.2; python_version<"3.8" +mkdocstrings[python-legacy]~=0.23.0 + diff --git a/tests/conftest.py b/tests/conftest.py index 47c216155..856167d10 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,11 @@ if TYPE_CHECKING: from ontopy.ontology import Ontology + from typing import Sequence +import sys, os + +# Add test-specific utilities to path +sys.path.append(os.path.join(os.path.dirname(__file__), "utilities")) @pytest.fixture(scope="session") @@ -16,7 +21,14 @@ def repo_dir() -> Path: @pytest.fixture def emmo() -> "Ontology": - """Load and return EMMO.""" + """Load and return EMMO. + + Note that this loads the version of EMMO that is + the current defailt in ontopy. + + When updating the defeault EMMO for ontopy, + tests might need to be updated. + """ from emmopy import get_emmo emmo = get_emmo() diff --git a/tests/ontopy_tests/conftest.py b/tests/ontopy_tests/conftest.py index b17d6ba12..d05a2cdca 100644 --- a/tests/ontopy_tests/conftest.py +++ b/tests/ontopy_tests/conftest.py @@ -5,43 +5,3 @@ # Files to skip collect_ignore = ["interactive_test.py"] - - -# Utilities -def abbreviate(onto, iri, must_exist=True): - """Returns existing Owlready2 storid for `iri`.""" - if iri is None: - return None - abbreviater = getattr(onto, "_abbreviate") - storid = abbreviater(iri, create_if_missing=False) - if storid is None and must_exist: - raise ValueError(f"no such IRI in ontology: {iri}") - return storid - - -def get_triples(onto, s=None, p=None, o=None) -> list: - """Returns a list of triples matching spo.""" - return [ - ( - onto._unabbreviate(s_) if isinstance(s_, int) and s_ > 0 else s_, - onto._unabbreviate(p_) if isinstance(p_, int) and p_ > 0 else p_, - onto._unabbreviate(o_) if isinstance(o_, int) and o_ > 0 else o_, - ) - for s_, p_, o_, d in onto._get_triples_spod_spod( - abbreviate(onto, s), - abbreviate(onto, p), - abbreviate(onto, o, False) or o, - None, - ) - ] - - -def has_triple(onto, s=None, p=None, o=None) -> bool: - """Returns true if ontology `onto` contains the given triple. - - None may be used as a wildcard for of `s`, `p` or `o`. - """ - try: - return bool(get_triples(onto, s, p, o)) - except ValueError: - return False diff --git a/tests/ontopy_tests/test_deprecation_warnings.py b/tests/ontopy_tests/test_deprecation_warnings.py new file mode 100644 index 000000000..90e2c4b9b --- /dev/null +++ b/tests/ontopy_tests/test_deprecation_warnings.py @@ -0,0 +1,20 @@ +from typing import TYPE_CHECKING +import pytest + +if TYPE_CHECKING: + from pathlib import Path + + +@pytest.mark.filterwarnings( + "ignore:Ontology.add_label_annotations() is deprecated. Direct modify the `label_annotations` attribute instead." +) +def test_deprecation_warnings() -> None: + """Test functionalities will be removed and currently have depracation warnings""" + from ontopy import get_ontology + from ontopy.ontology import DEFAULT_LABEL_ANNOTATIONS + + testonto = get_ontology("http://domain_ontology/new_ontology") + + testonto.add_label_annotation(DEFAULT_LABEL_ANNOTATIONS[0]) + + testonto.remove_label_annotation(DEFAULT_LABEL_ANNOTATIONS[0]) diff --git a/tests/ontopy_tests/test_new_entity.py b/tests/ontopy_tests/test_new_entity.py index 99794cf44..11437bba8 100644 --- a/tests/ontopy_tests/test_new_entity.py +++ b/tests/ontopy_tests/test_new_entity.py @@ -1,13 +1,5 @@ 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 @@ -15,6 +7,14 @@ def test_new_entity(testonto: "Ontology") -> None: """Test adding entities to ontology""" + from ontopy.utils import ( + NoSuchLabelError, + LabelDefinitionError, + EntityClassDefinitionError, + ) + from owlready2.entity import ThingClass + from owlready2.prop import ObjectPropertyClass, DataPropertyClass + from owlready2 import AnnotationPropertyClass # Add entity directly testonto.new_entity("FantasyClass", testonto.TestClass) @@ -33,6 +33,14 @@ def test_new_entity(testonto: "Ontology") -> None: testonto.new_entity( "AnotherClass", testonto.TestClass, entitytype=ThingClass ) + + testonto.new_entity( + "YetAnotherClass", + testonto.TestClass, + entitytype=ThingClass, + preflabel="YetAnotherClass", + ) + testonto.new_entity( "hasSubObjectProperty", testonto.hasObjectProperty, @@ -100,3 +108,18 @@ def test_new_entity(testonto: "Ontology") -> None: testonto.new_annotation_property( "hasSubAnnotationProperty3", testonto.hasAnnotationProperty ) + + +def test_new_entity_w_preflabel() -> None: + """Test adding entities to ontology""" + from ontopy import get_ontology + import owlready2 + + testonto2 = get_ontology("http://domain_ontology/new_ontology") + testonto2.new_entity( + "NewClass", + owlready2.Thing, + preflabel="NewClass", + ) + + # assert testonto2.NewClass.preflabel == "NewClass" diff --git a/tests/ontopy_tests/test_ontology_helper_functions.py b/tests/ontopy_tests/test_ontology_helper_functions.py new file mode 100644 index 000000000..d022ed99c --- /dev/null +++ b/tests/ontopy_tests/test_ontology_helper_functions.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING +import pytest +from owlready2.entity import ThingClass +from owlready2.prop import ObjectPropertyClass, DataPropertyClass +from owlready2 import AnnotationPropertyClass + +if TYPE_CHECKING: + from pathlib import Path + + +def test_ontology_to_storids(testonto: "Ontology") -> None: + """Test adding helper functions in ontopy.ontology""" + from ontopy.ontology import DEFAULT_LABEL_ANNOTATIONS + + label_annotations = DEFAULT_LABEL_ANNOTATIONS + assert len(testonto._to_storids(label_annotations)) == 3 + assert testonto._to_storids(None) == [] + assert testonto._to_storids([testonto.TestClass]) diff --git a/tests/ontopy_tests/test_patch.py b/tests/ontopy_tests/test_patch.py index d6704863f..8ce2b253b 100644 --- a/tests/ontopy_tests/test_patch.py +++ b/tests/ontopy_tests/test_patch.py @@ -1,48 +1,91 @@ """Tests Owlready2 patches implemented in ontopy/patch.py -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 +from utilities import setassert + + +def test_get_by_label_onto(emmo: "Ontology") -> None: + # Test some ThingClass extensions implemented in patch.py + assert str(emmo.Atom.get_preferred_label()) == "Atom" + + assert emmo.Atom.get_parents() == {emmo.MolecularEntity} + + setassert( + emmo.Atom.get_annotations().keys(), + { + "prefLabel", + "altLabel", + "elucidation", + "comment", + }, + ) + setassert( + emmo.Atom.get_annotations(all=True).keys(), + { + "qualifiedCardinality", + "minQualifiedCardinality", + "prefLabel", + "abstract", + "hiddenLabel", + "etymology", + "altLabel", + "example", + "elucidation", + "OWLDLRestrictedAxiom", + "wikipediaReference", + "conceptualisation", + "logo", + "comment", + "dbpediaReference", + "definition", + "VIMTerm", + "creator", + "iupacReference", + "contact", + "omReference", + "ISO9000Reference", + "ISO80000Reference", + "qudtReference", + "contributor", + "license", + "ISO14040Reference", + "figure", + "title", + "publisher", + }, + ) + + # Test item access/assignment/deletion for classes + setassert(emmo.Atom["altLabel"], {"ChemicalElement"}) + + with pytest.raises(KeyError): + emmo.Atom["hasPart"] + + emmo.Atom["altLabel"] = "Element" + setassert(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 + assert ( + emmo.wikipediaReference + ) # Check that wikipediaReference is in ontology + assert ( + emmo.Atom.wikipediaReference == [] + ) # Check that wikipediaReference can be acceses as attribute -emmo = get_ontology().load() - - -# Test some ThingClass extensions implemented in patch.py -assert emmo.Atom.get_preferred_label() == "Atom" - -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. diff --git a/tests/ontopy_tests/test_prefix.py b/tests/ontopy_tests/test_prefix.py index 7d17da56b..d8452adbe 100644 --- a/tests/ontopy_tests/test_prefix.py +++ b/tests/ontopy_tests/test_prefix.py @@ -1,6 +1,7 @@ from typing import TYPE_CHECKING import pytest from ontopy.utils import NoSuchLabelError +import warnings if TYPE_CHECKING: from pathlib import Path @@ -37,6 +38,12 @@ def test_prefix(testonto: "Ontology", emmo: "Ontology") -> None: "TestClass", prefix="models" ) + with pytest.raises(ValueError): + testonto.get_by_label_all(" ") + + with pytest.raises(TypeError): + testonto.get_by_label(1) + def test_prefix_emmo(emmo: "Ontology") -> None: """Test prefix in ontology""" diff --git a/tests/ontopy_tests/test_utils.py b/tests/ontopy_tests/test_utils.py index 4b5647682..1505ef03f 100644 --- a/tests/ontopy_tests/test_utils.py +++ b/tests/ontopy_tests/test_utils.py @@ -1,5 +1,5 @@ import ontopy.utils as utils -from testutils import get_triples, has_triple +from utilities import get_triples, has_triple def test_annotate_source(testonto: "Ontology"): diff --git a/tests/test_classes.py b/tests/test_classes.py new file mode 100644 index 000000000..b4fd7e072 --- /dev/null +++ b/tests/test_classes.py @@ -0,0 +1,56 @@ +import pytest + +from ontopy import get_ontology +from ontopy.ontology import NoSuchLabelError + + +def test_classes(repo_dir) -> None: + """Test that classes are returned from imported ontologies when + asked for, but not for the whole world + """ + import ontopy + import owlready2 + + world = ontopy.World() + testonto = world.get_ontology("http://domain_ontology/new_ontology") + testonto.new_entity("Class", owlready2.Thing) + + imported_onto = world.get_ontology( + repo_dir / "tests" / "testonto" / "testonto.ttl" + ).load() + testonto.imported_ontologies.append(imported_onto) + + assert set(testonto.classes(imported=True)) == { + testonto.TestClass, + testonto.get_by_label("models:TestClass"), + testonto.Class, + } + assert set(imported_onto.classes(imported=True)) == { + imported_onto.TestClass, + imported_onto.get_by_label("models:TestClass"), + } + assert set(imported_onto.classes(imported=False)) == { + imported_onto.TestClass + } + + assert ( + set(testonto.individuals(imported=True)) == set() + ) # We currently do not have examples with individuals. + assert set(testonto.individuals(imported=False)) == set() + + assert set(testonto.object_properties(imported=True)) == { + testonto.hasObjectProperty + } + assert set(testonto.object_properties(imported=False)) == set() + + assert set(testonto.annotation_properties(imported=True)) == { + testonto.prefLabel, + testonto.altLabel, + testonto.hasAnnotationProperty, + } + assert set(testonto.annotation_properties(imported=False)) == set() + + assert set(testonto.data_properties(imported=True)) == { + testonto.hasDataProperty + } + assert set(testonto.data_properties(imported=False)) == set() diff --git a/tests/test_dir.py b/tests/test_dir.py index 867fa7758..c43af81cd 100644 --- a/tests/test_dir.py +++ b/tests/test_dir.py @@ -2,7 +2,6 @@ from ontopy import get_ontology - thisdir = Path(__file__).resolve().parent onto = get_ontology( diff --git a/tests/test_emmocheck_module.py b/tests/test_emmocheck_module.py new file mode 100644 index 000000000..16e95b2aa --- /dev/null +++ b/tests/test_emmocheck_module.py @@ -0,0 +1,10 @@ +from emmopy.emmocheck import main + + +main( + argv=[ + "--url-from-catalog", + # Test against a specific commit of EMMO 1.0.0-beta5 + "https://raw.githubusercontent.com/emmo-repo/EMMO/3b93e2c9c45ab8d9882d2d6385276ff905095798/emmo.ttl", + ] +) diff --git a/tests/test_excelparser/onto.xlsx b/tests/test_excelparser/onto.xlsx old mode 100755 new mode 100644 index 2274d0018..3e7a07528 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 old mode 100755 new mode 100644 index 3ff88b994..2a37ccb74 Binary files a/tests/test_excelparser/onto_only_classes.xlsx 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 index f8c36d396..e2184bd93 100644 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 index f8c36d396..f3c2639ae 100644 Binary files a/tests/test_excelparser/onto_update_only_classes.xlsx 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 4b4c9f7fa..ae1a98580 100644 --- a/tests/test_excelparser/result_ontology/fromexcelonto.ttl +++ b/tests/test_excelparser/result_ontology/fromexcelonto.ttl @@ -12,8 +12,8 @@ "Jesper Friis"@en, "Sylvain Gouttebroze"@en ; dcterms:title "A test domain ontology"@en ; - owl:imports , - ; + owl:imports , + ; owl:versionInfo "0.01"@en . :EMMO_0264be35-e8ad-5b35-a1a3-84b37bde22d1 a owl:Class ; @@ -40,7 +40,7 @@ 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:EMMO_c84c6752_6d64_48cc_9500_e54a3c34898d "\"http at wikipedia\""@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 ; @@ -142,12 +142,13 @@ skos:prefLabel "Atom"@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 ; + rdfs:comment "Where to find the entry in the \"book of boundaries\""@en ; + rdfs:subPropertyOf emmo:EMMO_c7b62dd7_063a_4c2a_8504_42f7264ba83f ; 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:comment "This definition is humbug"@en, + "has a part that is a boundary"@en ; rdfs:domain :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; rdfs:subPropertyOf emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; skos:prefLabel "hasBoundaryPart"@en . @@ -157,28 +158,29 @@ rdfs:subClassOf :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 ; skos:prefLabel "SpatialBoundary"@en . +:EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "everything that can be perceived or measured"@en ; + rdfs:comment " note that this is changed from pattern as Pattern is from emmo-beta4 an altLabel for Data"@en, + " 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 ; + skos:prefLabel "SpecialPattern"@en . + :EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 a owl:Class ; - emmo:EMMO_21ae69b4_235e_479d_8dd8_4f756f694c1b "A"@en, + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf :EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 ; + skos:altLabel "A"@en, "Just"@en, "Test"@en ; - emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; - rdfs:subClassOf :EMMO_cd254842-c697-55f6-917d-9805c77b9187 ; 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 ; - 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 ; 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 ; + :EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 ; skos:prefLabel "FinitePattern"@en . :EMMO_1b2bfe71-5da9-5c46-b137-be45c3e3f9c3 a owl:Class ; diff --git a/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl b/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl index 5ded0b43f..cb6c2d276 100644 --- a/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl +++ b/tests/test_excelparser/result_ontology/fromexcelonto_only_classes.ttl @@ -1,19 +1,19 @@ @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 ; - owl:imports , - ; + dcterms:title "A test domain ontology"@en ; + owl:imports , + ; owl:versionInfo "0.01"@en . :EMMO_0264be35-e8ad-5b35-a1a3-84b37bde22d1 a owl:Class ; @@ -24,53 +24,53 @@ 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_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_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 ; 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 ], + [ a owl:Restriction ; + owl:onProperty emmo:EMMO_17e27c22_37e1_468c_9dd7_95e137f73e7f ; + owl:someValuesFrom :EMMO_472ed27e-ce08-53cb-8453-56ab363275c4 ], :EMMO_4b32833e-0833-56a7-903c-28a6a8191fe8, :EMMO_5f50f77e-f321-53e3-af76-fe5b0a347479 ; - core:prefLabel "FiniteSpatialPattern"@en . + skos: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 . + 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,59 +79,59 @@ 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_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 . + skos: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 . + skos:prefLabel "SpatialBoundary"@en . -:EMMO_cd254842-c697-55f6-917d-9805c77b9187 a owl:Class ; +:EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 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 "SpecialPattern"@en . + +:EMMO_9fa9ca88-2891-538a-a8dd-ccb8a08b9890 a owl:Class ; + emmo:EMMO_967080e5_2f42_4eb2_a3a9_c58143e835f9 "NEED elucidation"@en ; + rdfs:subClassOf :EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 ; + skos:altLabel "A"@en, + "Just"@en, + "Test"@en ; + skos:prefLabel "SpatioTemporalPattern"@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 ; @@ -139,5 +139,5 @@ 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 . + :EMMO_80bf0979-a0ec-529c-b9a1-d4aa3032e037 ; + skos:prefLabel "FinitePattern"@en . diff --git a/tests/test_excelparser/test_excelparser.py b/tests/test_excelparser/test_excelparser.py index 220596dec..0131b591d 100644 --- a/tests/test_excelparser/test_excelparser.py +++ b/tests/test_excelparser/test_excelparser.py @@ -32,7 +32,7 @@ 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) - ontology.save("test.ttl") + # ontology.save("test.ttl") # used for printing new ontology when debugging assert onto == ontology assert errors.keys() == { "already_defined", @@ -69,7 +69,7 @@ def test_excelparser(repo_dir: "Path") -> None: "data_prop_errors_in_range", "data_prop_errors_in_domain", } - assert errors["already_defined"] == {"Pattern"} + assert errors["already_defined"] == {"SpecialPattern"} assert errors["in_imported_ontologies"] == {"Atom"} assert errors["wrongly_defined"] == {"Temporal Boundary"} assert errors["missing_subClassOf"] == {"SpatioTemporalBoundary"} @@ -79,7 +79,7 @@ def test_excelparser(repo_dir: "Path") -> None: "SubgrainBoundary", } assert errors["nonadded_concepts"] == { - "Pattern", + "SpecialPattern", "Temporal Boundary", } @@ -116,8 +116,11 @@ def test_excelparser_only_classes(repo_dir: "Path") -> None: / "onto_update_only_classes.xlsx" ) ontology, catalog, errors = create_ontology_from_excel(xlspath, force=True) + # Used for printing new ontology when debugging + # ontology.save("test_only_classes.ttl") + assert onto == ontology - assert errors["already_defined"] == {"Pattern"} + assert errors["already_defined"] == {"SpecialPattern"} assert errors["in_imported_ontologies"] == {"Atom"} assert errors["wrongly_defined"] == {"Temporal Boundary"} assert errors["missing_subClassOf"] == {"SpatioTemporalBoundary"} @@ -127,7 +130,7 @@ def test_excelparser_only_classes(repo_dir: "Path") -> None: "SubgrainBoundary", } assert errors["nonadded_concepts"] == { - "Pattern", + "SpecialPattern", "Temporal Boundary", } diff --git a/tests/test_generation_search.py b/tests/test_generation_search.py index 3afe2c17a..f3ecaad54 100755 --- a/tests/test_generation_search.py +++ b/tests/test_generation_search.py @@ -147,3 +147,8 @@ def test_ancestors(emmo: "Ontology", repo_dir: "Path") -> None: onto.NorwaySpruce, onto.Avocado, } + + assert ( + onto.get_wu_palmer_measure(onto.NorwaySpruce, onto.Avocado) + == 0.5714285714285714 + ) diff --git a/tests/test_get_by_label.py b/tests/test_get_by_label.py index ac2686a21..5b84a5281 100644 --- a/tests/test_get_by_label.py +++ b/tests/test_get_by_label.py @@ -1,58 +1,110 @@ import pytest -from ontopy import get_ontology -from ontopy.ontology import NoSuchLabelError - -def test_get_by_label_onto() -> None: +def test_get_by_label_onto(repo_dir) -> None: """Test that label annotations are added correctly if they are not added before using get_by_label """ + from ontopy import get_ontology + from ontopy.ontology import NoSuchLabelError, DEFAULT_LABEL_ANNOTATIONS import owlready2 + # create ontology with one class and check that it is found testonto = get_ontology("http://domain_ontology/new_ontology") testonto.new_entity("Class", owlready2.Thing) - assert testonto._label_annotations == None - assert testonto.get_by_label("Class") == testonto.Class - - -def test_get_by_label_all_onto() -> None: - """Test that label annotations are added correctly if they are not added before - using get_by_label_all - """ - import owlready2 - testonto = get_ontology("http://domain_ontology/new_ontology") - testonto.new_entity("Class", owlready2.Thing) - assert testonto._label_annotations == None + assert testonto.label_annotations == DEFAULT_LABEL_ANNOTATIONS + assert testonto.get_by_label("Class") == testonto.Class assert testonto.get_by_label_all("*") == {testonto.Class} + testonto.new_annotation_property( "SpecialAnnotation", owlready2.AnnotationProperty ) testonto.Class.SpecialAnnotation.append("This is a comment") - testonto.set_default_label_annotations() testonto.new_entity("Klasse", testonto.Class) - assert testonto.Klasse.prefLabel == ["Klasse"] + with pytest.raises(AttributeError): + assert testonto.Klasse.prefLabel == ["Klasse"] + + assert testonto.get_by_label_all("*") == { + testonto.Class, + testonto.SpecialAnnotation, + testonto.Klasse, + } + + # Add prefLabel to ontology + preflabel = testonto.new_annotation_property( + "prefLabel", + parent=[owlready2.AnnotationProperty], + ) + preflabel.iri = "http://www.w3.org/2004/02/skos/core#prefLabel" + + # After prefLabel was added to the ontology, prefLabels can be accessed + with pytest.raises(AssertionError): + assert testonto.prefLabel.prefLabel == ["prefLabel"] + testonto.prefLabel.prefLabel = "prefLabel" + assert testonto.prefLabel.prefLabel == ["prefLabel"] + + with pytest.raises(AssertionError): + assert testonto.Klasse.prefLabel == ["Klasse"] + + testonto.new_entity("UnderKlasse", testonto.Klasse) + assert testonto.UnderKlasse.prefLabel.en == ["UnderKlasse"] - testonto.Klasse.altLabel = "Class2" assert testonto.get_by_label_all("*") == { testonto.prefLabel, - testonto.altLabel, testonto.Class, testonto.SpecialAnnotation, testonto.Klasse, + testonto.UnderKlasse, } assert testonto.get_by_label_all("Class*") == { testonto.Class, - testonto.Klasse, } + # Check that imported ontologies are searched + imported_onto = testonto.world.get_ontology( + repo_dir / "tests" / "testonto" / "testonto.ttl" + ).load() + testonto.imported_ontologies.append(imported_onto) + assert imported_onto.get_by_label("TestClass") + assert imported_onto.get_by_label("models:TestClass") + + assert testonto.get_by_label("TestClass") + + # Check exact_match=True in get_by_label_all() + assert testonto.get_by_label_all("*", exact_match=True) == set() + assert testonto.get_by_label_all("Clas*", exact_match=True) == set() + assert testonto.get_by_label_all("Class", exact_match=True) == { + testonto.Class, + } + + # Test with label annotations given directly when creating the ontology + testonto2 = get_ontology( + "http://domain_ontology/new_ontology", + label_annotations=[ + "http://www.w3.org/2004/02/skos/core#prefLabel", + "http://www.w3.org/2004/02/skos/core#altLabel", + ], + ) + + testonto2.new_annotation_property( + "prefLabel", + parent=[owlready2.AnnotationProperty], + ) + testonto2.new_entity("Class", owlready2.Thing, preflabel="Klasse") + + assert testonto2.get_by_label("Klasse") == testonto2.Class + def test_get_by_label_emmo(emmo: "Ontology") -> None: # Loading emmo-inferred where everything is sqashed into one ontology - emmo = get_ontology().load() + from emmopy import get_emmo + from ontopy import get_ontology + from ontopy.ontology import NoSuchLabelError + + emmo = get_emmo() assert emmo[emmo.Atom.name] == emmo.Atom assert emmo[emmo.Atom.iri] == emmo.Atom @@ -60,7 +112,7 @@ def test_get_by_label_emmo(emmo: "Ontology") -> None: onto = get_ontology( "https://raw.githubusercontent.com/BIG-MAP/BattINFO/master/battinfo.ttl" ).load() - assert onto.Electrolyte.prefLabel.first() == "Electrolyte" + assert onto.Electrolyte.prefLabel.en.first() == "Electrolyte" # Check colon_in_name argument onto.Atom.altLabel.append("Element:X") @@ -68,29 +120,3 @@ def test_get_by_label_emmo(emmo: "Ontology") -> None: onto.get_by_label("Element:X") assert onto.get_by_label("Element:X", colon_in_label=True) == onto.Atom - - -import pytest - -from ontopy import get_ontology -from ontopy.ontology import NoSuchLabelError - - -# Loading emmo-inferred where everything is sqashed into one ontology -emmo = get_ontology().load() -assert emmo[emmo.Atom.name] == emmo.Atom -assert emmo[emmo.Atom.iri] == emmo.Atom - -# Load an ontology with imported sub-ontologies -onto = get_ontology( - "https://raw.githubusercontent.com/BIG-MAP/BattINFO/master/battinfo.ttl" -).load() -assert onto.Electrolyte.prefLabel.first() == "Electrolyte" - - -# Check colon_in_name argument -onto.Atom.altLabel.append("Element:X") -with pytest.raises(NoSuchLabelError): - onto.get_by_label("Element:X") - -assert onto.get_by_label("Element:X", colon_in_label=True) == onto.Atom diff --git a/tests/test_load.py b/tests/test_load.py index e549b0e0e..665586254 100755 --- a/tests/test_load.py +++ b/tests/test_load.py @@ -12,28 +12,28 @@ def test_load(repo_dir: "Path", testonto: "Ontology") -> None: # Check that the defaults works emmo = get_ontology("emmo").load() # ttl format - assert emmo.Atom.prefLabel.first() == "Atom" + assert str(emmo.Atom.prefLabel.first()) == "Atom" emmo = get_ontology("emmo-inferred").load() - assert emmo.Atom.prefLabel.first() == "Atom" + assert str(emmo.Atom.prefLabel.first()) == "Atom" emmo = get_ontology("emmo-development").load() # ttl format - assert emmo.Atom.prefLabel.first() == "Atom" + assert str(emmo.Atom.prefLabel.first()) == "Atom" emmo = get_ontology( "https://emmo-repo.github.io/latest-stable/" "emmo-inferred.owl" ).load() # owl format - assert emmo.Atom.prefLabel.first() == "Atom" + assert str(emmo.Atom.prefLabel.first()) == "Atom" # Load a local ontology with catalog - assert testonto.TestClass.prefLabel.first() == "TestClass" + assert str(testonto.TestClass.prefLabel.first()) == "TestClass" # Use catalog file when downloading from web onto = get_ontology( "https://raw.githubusercontent.com/BIG-MAP/BattINFO/master/" "battinfo.ttl" ).load() - assert onto.Electrolyte.prefLabel.first() == "Electrolyte" + assert str(onto.Electrolyte.prefLabel.first()) == "Electrolyte" with pytest.raises( EMMOntoPyException, diff --git a/tests/ontopy_tests/testutils.py b/tests/utilities/utilities.py similarity index 78% rename from tests/ontopy_tests/testutils.py rename to tests/utilities/utilities.py index 6b18c4953..c4dc9667e 100644 --- a/tests/ontopy_tests/testutils.py +++ b/tests/utilities/utilities.py @@ -1,6 +1,15 @@ -"""Test utility functions.""" +from pathlib import Path +from typing import TYPE_CHECKING +import pytest + +if TYPE_CHECKING: + from ontopy.ontology import Ontology + from typing import Sequence + + +# Utilities def abbreviate(onto, iri, must_exist=True): """Returns existing Owlready2 storid for `iri`.""" if iri is None: @@ -38,3 +47,7 @@ def has_triple(onto, s=None, p=None, o=None) -> bool: return bool(get_triples(onto, s, p, o)) except ValueError: return False + + +def setassert(values: "Sequence(str)", expected: "Sequence(str)") -> None: + assert set(str(val) for val in values) == set(str(val) for val in expected)