diff --git a/.github/workflows/ci_workflow.yml b/.github/workflows/ci_workflow.yml index daaa282cc..8eae7c867 100644 --- a/.github/workflows/ci_workflow.yml +++ b/.github/workflows/ci_workflow.yml @@ -70,7 +70,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10", "3.11"] + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] steps: - name: Checkout repository diff --git a/docs/tools-instructions.md b/docs/tools-instructions.md index 6ef8a32cf..b03df4f9f 100644 --- a/docs/tools-instructions.md +++ b/docs/tools-instructions.md @@ -69,15 +69,40 @@ optional arguments: ``` -### Example configuration file -Example of YAML configuration file provided with the `--configfile` option that will omit `myunits.MyUnitCategory1` and `myunits.MyUnitCategory1` from the *unit dimensions test*. +### Configuration file +The `--configfile` options expects a YAML configuration file that specifies what tests to skip or enable. + +The following keywords are recognised in the YAML file: + + - `skip`: List of tests to skip + - `enable`: List of tests to enable + - ``: A name of a test. Recognised nested keywords are: + - `exceptions`: List of entities in the ontology to skip. Should be written + as `.`, where `` is the last component of the base IRI + and `` is the name of the entity. + - `skipmodules`: List of module names to skip the test for. The module + names may be written either as the full module IRI or as the last + component of the module IRI. + +Example configuration file: ```console +test_description: + skipmodules: + - manufacturing + - conformityassessment + test_unit_dimensions: exceptions: - myunits.MyUnitCategory1 - myunits.MyUnitCategory2 + +skip: + - name_of_test_to_skip + +enable: + - name_of_test_to_enable ``` --- diff --git a/emmopy/emmocheck.py b/emmopy/emmocheck.py index 765df491f..89843d0e5 100644 --- a/emmopy/emmocheck.py +++ b/emmopy/emmocheck.py @@ -1,11 +1,29 @@ # -*- coding: utf-8 -*- +# pylint: disable=too-many-lines,invalid-name """ A module for testing an ontology against conventions defined for EMMO. A YAML file can be provided with additional test configurations. +Toplevel keywords in the YAML file: + + - `skip`: List of tests to skip + - `enable`: List of tests to enable + - ``: A name of a test. Recognised nested keywords are: + - `exceptions`: List of entities in the ontology to skip. Should be written + as `.`, where `` is the last component of the base IRI + and `` is the name of the entity. + - `skipmodules`: List of module names to skip the test for. The module + names may be written either as the full module IRI or as the last + component of the module IRI. + Example configuration file: + test_description: + skipmodules: + - manufacturing + - conformityassessment + test_unit_dimensions: exceptions: - myunits.MyUnitCategory1 @@ -194,6 +212,12 @@ def test_description(self): Exceptions include entities from standard w3c vocabularies. """ + # pylint: disable=invalid-name + MeasurementUnit = ( + self.onto.MeasurementUnit + if "MeasurementUnit" in self.onto + else None + ) exceptions = set() exceptions.update(self.get_config("test_description.exceptions", ())) props = self.onto.world._props # pylint: disable=protected-access @@ -214,6 +238,23 @@ def test_description(self): if r in exceptions or any(r.startswith(v) for v in vocabs): continue + # Skip units subclasses with a physical dimension + if ( + MeasurementUnit + and issubclass(entity, MeasurementUnit) + and any( + str(r.property.prefLabel.first()) == "hasDimensionString" + for r in entity.get_indirect_is_a() + if hasattr(r, "property") + and hasattr(r.property, "prefLabel") + ) + ): + continue + + # Check skipmodules + if skipmodule(self, "test_description", entity): + continue + label = str(get_label(entity)) with self.subTest(entity=entity, label=label): self.assertTrue( @@ -314,6 +355,10 @@ def test_unit_dimension(self): "emmo.SIBaseUnit", "emmo.SIUnitSymbol", "emmo.SIUnit", + "emmo.SIAcceptedDerivedUnit", + "emmo.SIDerivedUnit", + "emmo.SIAcceptedPrefixedUnit", + "emmo.CGSUnit", ) ) if not hasattr(self.onto, "MeasurementUnit"): @@ -378,6 +423,11 @@ def test_unit_dimension_rc1(self): "emmo.SIBaseUnit", "emmo.SIUnitSymbol", "emmo.SIUnit", + "emmo.SIDerivedUnit", + "emmo.SIAcceptedPrefixedUnit", + "emmo.SIAcceptedDerivedUnit", + "emmo.SIMetricPrefixedUnit", + "emmo.CGSUnit", ) ) if not hasattr(self.onto, "MeasurementUnit"): @@ -496,6 +546,7 @@ def test_quantity_dimension(self): "metrology.ExactConstant", "metrology.MeasuredConstant", "metrology.DerivedQuantity", + "metrology.PhysicalQuantiyByDefinition", "isq.ISQBaseQuantity", "isq.InternationalSystemOfQuantity", "isq.ISQDerivedQuantity", @@ -531,6 +582,7 @@ def test_quantity_dimension(self): "emmo.Intensive", "emmo.Extensive", "emmo.Concentration", + "emmo.PhysicalQuantiyByDefinition", ) ) if not hasattr(self.onto, "PhysicalQuantity"): @@ -565,7 +617,7 @@ def test_quantity_dimension(self): issubclass(cls, self.onto.ISQDimensionlessQuantity) ) - def test_dimensional_unit(self): + def test_dimensional_unit_rc2(self): """Check correct syntax of dimension string of dimensional units.""" # This test requires that the ontology has imported SIDimensionalUnit @@ -585,6 +637,38 @@ def test_dimensional_unit(self): self.assertIsInstance(r, owlready2.Restriction) self.assertRegex(r.value, regex) + def test_dimensional_unit(self): + """Check correct syntax of dimension string of dimensional units.""" + + # This test requires that the ontology has imported SIDimensionalUnit + if "SIDimensionalUnit" not in self.onto: + self.skipTest("SIDimensionalUnit is not imported") + + # 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)): + dimstr = [ + r.value + for r in cls.is_a + if isinstance(r, owlready2.Restriction) + and repr(r.property) == "emmo.hasDimensionString" + ] + self.assertEqual( + len(dimstr), + 1, + msg="expect one emmo:hasDimensionString value restriction", + ) + self.assertRegex( + dimstr[0], + regex, + msg=f"invalid dimension string: '{dimstr[0]}'", + ) + def test_physical_quantity_dimension(self): """Check that all physical quantities have `hasPhysicalDimension`. @@ -734,6 +818,32 @@ def checker(onto, ignore_namespace): checker(self.onto, self.ignore_namespace) +def skipmodule(testobj, testname, entity): + """Return true if `entity` is in a module that should be skipped.""" + skipmodules = testobj.get_config(f"{testname}.skipmodules") + + if not skipmodules: + return False + + # Infer base iri + if entity.namespace.ontology.base_iri != "https://w3id.org/emmo#": + base_iri = entity.namespace.ontology.base_iri.rstrip("/#") + elif hasattr(entity, "isDefinedBy") and entity.isDefinedBy: + base_iri = entity.isDefinedBy.first().rstrip("/#") + else: + base_iri = entity.namespace.ontology.base_iri.rstrip("/#") + + for module in skipmodules: + module = module.rstrip("/#") + if "/" in module: + if module == base_iri: + return True + elif module == base_iri.rsplit("/", 1)[-1]: + return True + + return False + + def main( argv: list = None, ): # pylint: disable=too-many-locals,too-many-branches,too-many-statements @@ -938,6 +1048,7 @@ def main( "test_physical_quantity_dimension_annotation", "test_quantity_dimension_beta3", "test_physical_quantity_dimension", + "test_dimensional_unit_rc2", ] ) msg = {name: "skipped by default" for name in skipped} diff --git a/setup.py b/setup.py index 4084811b8..37b1acc4b 100644 --- a/setup.py +++ b/setup.py @@ -91,11 +91,11 @@ def fglob(patt): "License :: OSI Approved :: BSD License", "Operating System :: OS Independent", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", "Topic :: Scientific/Engineering :: Visualization", diff --git a/tests/test_basic.py b/tests/test_basic.py index 670ac5de0..3c0ab0922 100755 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -49,7 +49,7 @@ class H2O(emmo.Molecule): emmo.hasSpatialDirectPart.exactly(2, onto.Hydrogen) emmo.hasSpatialDirectPart.exactly(1, Oxygen) - # Create some + # Create some individuals H1 = onto.Hydrogen() H2 = onto.Hydrogen() O = Oxygen() @@ -59,7 +59,7 @@ class H2O(emmo.Molecule): name_prefix = "myonto_" onto.sync_attributes(name_policy="sequential", name_prefix=name_prefix) assert f"{onto.base_iri}{name_prefix}0" in onto - assert f"{onto.base_iri}{name_prefix}6" in onto + assert f"{onto.base_iri}{name_prefix}3" in onto name_prefix = "onto_" onto.sync_attributes(name_policy="uuid", name_prefix=name_prefix)