Skip to content

emmocheck

A module for testing an ontology against conventions defined for EMMO.

A YAML file can be provided with additional test configurations.

Example configuration file:

test_unit_dimensions:
  exceptions:
    - myunits.MyUnitCategory1
    - myunits.MyUnitCategory2

skip:
  - name_of_test_to_skip

enable:
  - name_of_test_to_enable

TestEMMOConventions

Base class for testing an ontology against EMMO conventions.

get_config(self, string, default=None)

Returns the configuration specified by string.

If configuration is not found in the configuration file, default is returned.

Sub-configurations can be accessed by separating the components with dots, like "test_namespace.exceptions".

Source code in emmopy/emmocheck.py
def get_config(self, string, default=None):
    """Returns the configuration specified by `string`.

    If configuration is not found in the configuration file, `default`
    is returned.

    Sub-configurations can be accessed by separating the components with
    dots, like "test_namespace.exceptions".
    """
    c = self.config
    try:
        for token in string.split('.'):
            c = c[token]
    except KeyError:
        return default
    return c

TestFunctionalEMMOConventions

Test functional EMMO conventions.

test_namespace(self)

Check that all IRIs are namespaced after their (sub)ontology.

Configurations

exceptions - full name of entities to ignore.

Source code in emmopy/emmocheck.py
def test_namespace(self):
    """Check that all IRIs are namespaced after their (sub)ontology.

    Configurations:
        exceptions - full name of entities to ignore.
    """
    exceptions = set((
        'owl.qualifiedCardinality',
        'owl.minQualifiedCardinality',
        'terms.creator',
        'terms.contributor',
        'terms.publisher',
        'terms.title',
        'terms.license',
        'terms.abstract',
        'core.prefLabel',
        'core.altLabel',
        'core.hiddenLabel',
        'mereotopology.Item',
        'manufacturing.EngineeredMaterial',
    ))
    exceptions.update(self.get_config('test_namespace.exceptions', ()))

    def checker(onto, ignore_namespace):
        if list(filter(onto.base_iri.strip('#').endswith,
                       self.ignore_namespace)) != []:
            print('Skipping namespace: ' + onto.base_iri)
            return
        entities = itertools.chain(onto.classes(),
                                   onto.object_properties(),
                                   onto.data_properties(),
                                   onto.individuals(),
                                   onto.annotation_properties())
        for e in entities:
            if e not in visited and repr(e) not in exceptions:
                visited.add(e)
                with self.subTest(
                        iri=e.iri, base_iri=onto.base_iri, entity=repr(e)):
                    self.assertTrue(
                        e.iri.endswith(e.name),
                        msg='the final part of entity IRIs must be their '
                        'name')
                    self.assertEqual(
                        e.iri, onto.base_iri + e.name,
                        msg='IRI %r does not correspond to module '
                        'namespace: %r' % (e.iri, onto.base_iri))

        if self.check_imported:
            for imp_onto in onto.imported_ontologies:
                if imp_onto not in visited_onto:
                    visited_onto.add(imp_onto)
                    checker(imp_onto, ignore_namespace)

    visited = set()
    visited_onto = set()
    checker(self.onto, self.ignore_namespace)

test_physical_quantity_dimension(self)

Check that all physical quantities have hasPhysicalDimension.

Note: this test will fail before isq is moved to emmo/domain.

Configurations

exceptions - full class names of classes to ignore.

Source code in emmopy/emmocheck.py
def test_physical_quantity_dimension(self):
    """Check that all physical quantities have `hasPhysicalDimension`.

    Note: this test will fail before isq is moved to emmo/domain.

    Configurations:
        exceptions - full class names of classes to ignore.

    """
    exceptions = set((
        'emmo.ModelledQuantitativeProperty',
        'emmo.MeasuredQuantitativeProperty',
        'emmo.ConventionalQuantitativeProperty',

        'emmo.QuantitativeProperty',
        '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.AtomicAndNuclearPhysicsQuantity',
        'emmo.ThermodynamicalQuantity',
        'emmo.LightAndRadiationQuantity',
        'emmo.SpaceAndTimeQuantity',
        'emmo.AcousticQuantity',
        'emmo.PhysioChememicalQuantity',
        'emmo.ElectromagneticQuantity',
        'emmo.MechanicalQuantity',
        'emmo.CondensedMatterPhysicsQuantity',
        'emmo.ChemicalCompositionQuantity',

        'emmo.Extensive',
        'emmo.Intensive',
    ))
    if not hasattr(self.onto, 'PhysicalQuantity'):
        return
    exceptions.update(
        self.get_config('test_physical_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 repr(cls) not in exceptions:
            with self.subTest(cls=cls, label=get_label(cls)):
                try:
                    class_props = cls.INDIRECT_get_class_properties()
                except AttributeError:
                    # The INDIRECT_get_class_properties() method
                    # does not support inverse properties.  Build
                    # class_props manually...
                    class_props = set()
                    for c in cls.mro():
                        if hasattr(c, 'is_a'):
                            class_props.update(
                                [r.property for r in c.is_a
                                 if isinstance(r, owlready2.Restriction)])

                self.assertIn(self.onto.hasPhysicalDimension,
                              class_props, msg=cls)

test_quantity_dimension(self)

Check that all quantities have a physicalDimension annotation.

Note: this test will be deprecated when isq is moved to emmo/domain.

Configurations

exceptions - full class names of classes to ignore.

Source code in emmopy/emmocheck.py
def test_quantity_dimension(self):
    """Check that all quantities have a physicalDimension annotation.

    Note: this test will be deprecated when isq is moved to emmo/domain.

    Configurations:
        exceptions - full class names of classes to ignore.
    """
    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.AtomicAndNuclear',
        'emmo.Defined',
        'emmo.Electromagnetic',
        'emmo.FrequentlyUsed',
        'emmo.PhysicoChemical',
        'emmo.ChemicalCompositionQuantity',
        'emmo.Universal',
    ))
    if not hasattr(self.onto, 'PhysicalQuantity'):
        return
    exceptions.update(
        self.get_config('test_quantity_dimension.exceptions', ()))
    regex = re.compile(
        '^T([+-][1-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)$')
    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 repr(cls) not in exceptions:
            with self.subTest(cls=cls, label=get_label(cls)):
                anno = cls.get_annotations()
                self.assertIn('physicalDimension', anno, msg=cls)
                physdim = anno['physicalDimension'].first()
                self.assertRegex(physdim, regex, msg=cls)

test_unit_dimension(self)

Check that all measurement units have a physical dimension.

Configurations

exceptions - full class names of classes to ignore.

Source code in emmopy/emmocheck.py
def test_unit_dimension(self):
    """Check that all measurement units have a physical dimension.

    Configurations:
        exceptions - full class names of classes to ignore.
    """
    exceptions = set((
        'metrology.MultipleUnit',
        'metrology.SubMultipleUnit',
        'metrology.OffSystemUnit',
        'metrology.PrefixedUnit',
        'metrology.NonPrefixedUnit',
        'metrology.SpecialUnit',
        'metrology.DerivedUnit',
        'metrology.BaseUnit',
        'metrology.UnitSymbol',
        'siunits.SICoherentDerivedUnit',
        'siunits.SINonCoherentDerivedUnit',
        'siunits.SISpecialUnit',
        'siunits.SICoherentUnit',
        'siunits.SIPrefixedUnit',
        'siunits.SIBaseUnit',
        'siunits.SIUnitSymbol',
        'siunits.SIUnit',

        'emmo.MultipleUnit',
        'emmo.SubMultipleUnit',
        'emmo.OffSystemUnit',
        'emmo.PrefixedUnit',
        'emmo.NonPrefixedUnit',
        'emmo.SpecialUnit',
        'emmo.DerivedUnit',
        'emmo.BaseUnit',
        'emmo.UnitSymbol',

        'emmo.SICoherentDerivedUnit',
        'emmo.SINonCoherentDerivedUnit',
        'emmo.SISpecialUnit',
        'emmo.SICoherentUnit',
        'emmo.SIPrefixedUnit',
        'emmo.SIBaseUnit',
        'emmo.SIUnitSymbol',
        'emmo.SIUnit',
    ))
    if not hasattr(self.onto, 'MeasurementUnit'):
        return
    exceptions.update(
        self.get_config('test_unit_dimension.exceptions', ()))
    regex = re.compile(
        r'^(emmo|metrology).hasPhysicalDimension.some\(.*\)$')
    classes = set(self.onto.classes(self.check_imported))
    for cls in self.onto.MeasurementUnit.descendants():
        if not self.check_imported and cls not in classes:
            continue
        # Assume that actual units are not subclassed
        if not list(cls.subclasses()) and repr(cls) not in exceptions:
            with self.subTest(cls=cls, label=get_label(cls)):
                self.assertTrue(
                    any(regex.match(repr(r))
                        for r in cls.get_indirect_is_a()), msg=cls)

TestSyntacticEMMOConventions

Test syntactic EMMO conventions.

test_class_label(self)

Check that class labels are CamelCase and valid identifiers.

For CamelCase, we are currently only checking that the labels start with upper case.

Source code in emmopy/emmocheck.py
def test_class_label(self):
    """Check that class labels are CamelCase and valid identifiers.

    For CamelCase, we are currently only checking that the labels
    start with upper case.
    """
    exceptions = set((
        '0-manifold',  # not needed in 1.0.0-beta
        '1-manifold',
        '2-manifold',
        '3-manifold',
        'C++',
    ))
    exceptions.update(self.get_config('test_class_label.exceptions', ()))

    for cls in self.onto.classes(self.check_imported):
        for label in cls.label + getattr(cls, 'prefLabel', []):
            if label not in exceptions:
                with self.subTest(entity=cls, label=label):
                    self.assertTrue(label.isidentifier())
                    self.assertTrue(label[0].isupper())

test_number_of_labels(self)

Check that all entities have one and only one prefLabel.

Use "altLabel" for synonyms.

The only allowed exception is entities who's representation starts with "owl.".

Source code in emmopy/emmocheck.py
def test_number_of_labels(self):
    """Check that all entities have one and only one prefLabel.

    Use "altLabel" for synonyms.

    The only allowed exception is entities who's representation
    starts with "owl.".
    """
    exceptions = set((
        'terms.license',
        'terms.abstract',
        'terms.contributor',
        'terms.creator',
        'terms.publisher',
        'terms.title',
        'core.prefLabel',
        'core.altLabel',
        'core.hiddenLabel',
    ))
    exceptions.update(self.get_config(
        'test_number_of_labels.exceptions', ()))

    if 'prefLabel' in self.onto.world._props:
        for e in self.onto.get_entities():
            if repr(e) not in exceptions:
                with self.subTest(entity=e, label=get_label(e),
                                  prefLabels=e.prefLabel):
                    if not repr(e).startswith('owl.'):
                        self.assertTrue(hasattr(e, 'prefLabel'))
                        self.assertEqual(1, len(e.prefLabel))
    else:
        self.fail('ontology has no prefLabel')

test_object_property_label(self)

Check that object property labels are lowerCamelCase.

Allowed exceptions: "EMMORelation"

If they start with "has" or "is" they should be followed by a upper case letter.

If they start with "is" they should also end with "Of".

Source code in emmopy/emmocheck.py
def test_object_property_label(self):
    """Check that object property labels are lowerCamelCase.

    Allowed exceptions: "EMMORelation"

    If they start with "has" or "is" they should be followed by a
    upper case letter.

    If they start with "is" they should also end with "Of".
    """
    exceptions = set((
        'EMMORelation',
    ))
    exceptions.update(self.get_config(
        'test_object_property_label.exceptions', ()))

    for op in self.onto.object_properties():
        if repr(op) not in exceptions:
            for label in op.label:
                with self.subTest(entity=op, label=label):
                    self.assertTrue(label[0].islower(),
                                    'label start with lowercase')
                    if label.startswith('has'):
                        self.assertTrue(label[3].isupper(),
                                        'what follows "has" must be '
                                        'uppercase')
                    if label.startswith('is'):
                        self.assertTrue(label[2].isupper(),
                                        'what follows "is" must be '
                                        'uppercase')
                        self.assertTrue(label.endswith(('Of', 'With')),
                                        'should end with "Of" or "With"')

main()

Run all checks on ontology iri. Default is 'http://emmo.info/emmo'.

Source code in emmopy/emmocheck.py
def main():
    """Run all checks on ontology `iri`.  Default is 'http://emmo.info/emmo'.
    """
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        'iri',
        help='File name or URI to the ontology to test.')
    parser.add_argument(
        '--database', '-d', metavar='FILENAME', default=':memory:',
        help='Load ontology from Owlready2 sqlite3 database.  The `iri` '
        'argument should in this case be the IRI of the ontology you '
        'want to check.')
    parser.add_argument(
        '--local', '-l', action='store_true',
        help='Load imported ontologies locally.  Their paths are specified '
        'in Protègè catalog files or via the --path option.  The IRI should '
        'be a file name.')
    parser.add_argument(
        '--catalog-file', default='catalog-v001.xml',
        help='Name of Protègè catalog file in the same folder as the '
        'ontology.  This option is used together with --local and '
        'defaults to "catalog-v001.xml".')
    parser.add_argument(
        '--path', action='append', default=[],
        help='Paths where imported ontologies can be found.  May be provided '
        'as a comma-separated string and/or with multiple --path options.')
    parser.add_argument(
        '--check-imported', '-i', action='store_true',
        help='Whether to check imported ontologies.')
    parser.add_argument(
        '--verbose', '-v', action='store_true',
        help='Verbosity level.')
    parser.add_argument(
        '--configfile', '-c',
        help='A yaml file with additional test configurations.')
    parser.add_argument(
        '--skip', '-s', action='append', default=[],
        help=('Shell pattern matching tests to skip.  This option may be '
              'provided multiple times.'))
    parser.add_argument(
        '--enable', '-e', action='append', default=[],
        help=('Shell pattern matching tests to enable that have been '
              'skipped by default or in the config file.  This option may '
              'be provided multiple times.'))
    parser.add_argument(  # deprecated, replaced by --no-catalog
        '--url-from-catalog', '-u', default=None, action='store_true',
        help=('Get url from catalog file'))
    parser.add_argument(
        '--no-catalog', action='store_false', dest='url_from_catalog',
        default=None,
        help='Whether to not read catalog file even if it exists.')
    parser.add_argument(
        '--ignore-namespace', '-n', action='append', default=[],
        help=('Namespace to be ignored. Can be given multiple '
              'times'))

    # Options to pass forward to unittest
    parser.add_argument(
        '--buffer', '-b',
        dest='unittest', action='append_const', const='-b',
        help=('The standard output and standard error streams are buffered '
              'during the test run. Output during a passing test is '
              'discarded. Output is echoed normally on test fail or error '
              'and is added to the failure messages.'))
    parser.add_argument(
        '--catch',
        dest='unittest', action='append_const', const='-c',
        help=('Control-C during the test run waits for the current test to '
              'end and then reports all the results so far. A second '
              'control-C raises the normal KeyboardInterrupt exception'))
    parser.add_argument(
        '--failfast', '-f',
        dest='unittest', action='append_const', const='-f',
        help=('Stop the test run on the first error or failure.'))
    try:
        args = parser.parse_args()
        sys.argv[1:] = args.unittest if args.unittest else []
        if args.verbose:
            sys.argv.append('-v')
    except SystemExit as e:
        os._exit(e.code)  # Exit without traceback on invalid arguments

    # Append to onto_path
    for paths in args.path:
        for path in paths.split(','):
            if path not in onto_path:
                onto_path.append(path)

    # Load ontology
    world = World(filename=args.database)
    if args.database != ':memory:' and args.iri not in world.ontologies:
        parser.error('The IRI argument should be one of the ontologies in '
                     'the database:\n  ' +
                     '\n  '.join(world.ontologies.keys()))

    onto = world.get_ontology(args.iri)
    onto.load(only_local=args.local,
              url_from_catalog=args.url_from_catalog,
              catalog_file=args.catalog_file)

    # Store settings TestEMMOConventions
    TestEMMOConventions.onto = onto
    TestEMMOConventions.check_imported = args.check_imported
    TestEMMOConventions.ignore_namespace = args.ignore_namespace

    # Configure tests
    verbosity = 2 if args.verbose else 1
    if args.configfile:
        import yaml
        with open(args.configfile, 'rt') as f:
            TestEMMOConventions.config.update(
                yaml.load(f, Loader=yaml.SafeLoader))

    # Run all subclasses of TestEMMOConventions as test suites
    status = 0
    for cls in TestEMMOConventions.__subclasses__():
        suite = unittest.TestLoader().loadTestsFromTestCase(cls)

        # Mark tests to be skipped
        for test in suite:
            name = test.id().split('.')[-1]
            skipped = set([  # skipped by default
                'test_namespace',
                'test_physical_quantity_dimension',
            ])
            msg = {name: 'skipped by default' for name in skipped}

            # enable/skip tests from config file
            for pattern in test.get_config('enable', ()):
                if fnmatch.fnmatchcase(name, pattern):
                    skipped.remove(name)
            for pattern in test.get_config('skip', ()):
                if fnmatch.fnmatchcase(name, pattern):
                    skipped.add(name)
                    msg[name] = 'skipped from config file'

            # enable/skip from command line
            for pattern in args.enable:
                if fnmatch.fnmatchcase(name, pattern):
                    skipped.remove(name)
            for pattern in args.skip:
                if fnmatch.fnmatchcase(name, pattern):
                    skipped.add(name)
                    msg[name] = 'skipped from command line'

            if name in skipped:
                setattr(test, 'setUp',
                        lambda: test.skipTest(msg.get(name, '')))

        runner = TextTestRunner(verbosity=verbosity)
        runner.resultclass.checkmode = True
        result = runner.run(suite)
        if result.failures:
            status = 1

    return status
Back to top