diff --git a/osp/core/ontology/docs/dcat2.yml b/osp/core/ontology/docs/dcat2.yml index 359e0656..cb7a6966 100644 --- a/osp/core/ontology/docs/dcat2.yml +++ b/osp/core/ontology/docs/dcat2.yml @@ -2,6 +2,15 @@ identifier: dcat2 ontology_file: https://www.w3.org/ns/dcat2.rdf reference_by_label: False +requirements: + - dcterms + - prov + - foaf namespaces: dcat2: http://www.w3.org/ns/dcat# -active_relationships: [] \ No newline at end of file + +active_relationships: + - http://www.w3.org/ns/dcat#catalog + - http://www.w3.org/ns/dcat#dataset + - http://www.w3.org/ns/dcat#service + - http://www.w3.org/ns/dcat#record \ No newline at end of file diff --git a/osp/core/ontology/docs/dcmitype.yml b/osp/core/ontology/docs/dcmitype.yml new file mode 100644 index 00000000..1c75da2f --- /dev/null +++ b/osp/core/ontology/docs/dcmitype.yml @@ -0,0 +1,6 @@ +identifier: dcmitype +ontology_file: https://www.dublincore.org/specifications/dublin-core/dcmi-terms/dublin_core_type.ttl +reference_by_label: False +namespaces: + dcmitype: "http://purl.org/dc/dcmitype/" +active_relationships: [] \ No newline at end of file diff --git a/osp/core/ontology/docs/dcterms.yml b/osp/core/ontology/docs/dcterms.yml new file mode 100644 index 00000000..447d9501 --- /dev/null +++ b/osp/core/ontology/docs/dcterms.yml @@ -0,0 +1,9 @@ +--- +identifier: dcterms +ontology_file: https://www.dublincore.org/specifications/dublin-core/dcmi-terms/dublin_core_terms.ttl +reference_by_label: False +requirements: + - dcmitype +namespaces: + dcterms: "http://purl.org/dc/terms/" +active_relationships: [] \ No newline at end of file diff --git a/osp/core/ontology/docs/emmo-datamodel.yml b/osp/core/ontology/docs/emmo-datamodel.yml new file mode 100644 index 00000000..3aca5746 --- /dev/null +++ b/osp/core/ontology/docs/emmo-datamodel.yml @@ -0,0 +1,7 @@ +identifier: emmo-datamodel +ontology_file: https://emmo-repo.github.io/datamodel-ontology/versions/0.0.2/metamodel-inferred.ttl +reference_by_label: Treu +namespaces: + datamodel: http://emmo.info/datamodel# +active_relationships: + - "http://emmo.info/datamodel#composition" \ No newline at end of file diff --git a/osp/core/ontology/docs/prov.yml b/osp/core/ontology/docs/prov.yml new file mode 100644 index 00000000..5b5ee5ee --- /dev/null +++ b/osp/core/ontology/docs/prov.yml @@ -0,0 +1,8 @@ +--- +identifier: prov +ontology_file: http://www.w3.org/ns/prov-o +format: ttl +reference_by_label: False +namespaces: + prov: http://www.w3.org/ns/prov# +active_relationships: [] diff --git a/osp/core/ontology/installation.py b/osp/core/ontology/installation.py index bddabdd7..6f6b6ac4 100644 --- a/osp/core/ontology/installation.py +++ b/osp/core/ontology/installation.py @@ -6,10 +6,12 @@ import shutil import sys import tempfile +from typing import Dict, Set from osp.core.ontology.parser.parser import OntologyParser logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) class OntologyInstallationManager: @@ -120,7 +122,107 @@ def _get_remaining_packages(self, remove_packages): "installed ontology package. " "Make sure to only specify valid " "yml files or ontology package names." % pkg) - return [v for k, v in installed_pkgs.items() if k not in remove_pkgs] + + remaining_packages = { + k: v for k, v in installed_pkgs.items() if k not in remove_pkgs + } + + # Block package removal if another package depends on it. + remaining_packages_requirements = { + name: OntologyParser.get_parser(path).requirements + for name, path in remaining_packages.items() + } + all_conflicts = self._resolve_dependencies_removal( + remaining_packages_requirements, + dict(), + set(remove_pkgs) + ) + if all_conflicts: + """Raise an exception.""" + message = "Cannot remove package{plural} {cannot_remove}{comma} " \ + "because other installed packages depend on {pronoun}: "\ + "{dependency_list}. " \ + "Please remove the packages {all_packages_to_remove} " \ + "all together." + cannot_remove = set(conflict + for conflicts in all_conflicts.values() + for conflict in conflicts + if conflict in remove_pkgs) + plural = "s" if len(cannot_remove) > 1 else "" + comma = ";" if plural else "," + pronoun = "them" if plural else "it" + cannot_remove = ', '.join(cannot_remove) + one_dependency = next(iter(all_conflicts.values())) + all_dependencies_equal = all( + one_dependency == x for x in all_conflicts.values() + ) + if all_dependencies_equal: + dependency_list = ', '.join(all_conflicts) + else: + dependency_list = set() + for package, conflicts in all_conflicts.items(): + dependency_list.add(f"package {package} depends on " + f"{', '.join(conflicts)}") + dependency_list = '; '.join(dependency_list) + all_packages_to_remove = set(all_conflicts) | remove_pkgs + last_package = all_packages_to_remove.pop() + all_packages_to_remove = ', '.join( + all_packages_to_remove + ) + ' and ' + last_package + message = message.format( + plural=plural, cannot_remove=cannot_remove, + comma=comma, pronoun=pronoun, dependency_list=dependency_list, + all_packages_to_remove=all_packages_to_remove, + ) + raise RuntimeError(message) + + return [v for v in remaining_packages.values()] + + def _resolve_dependencies_removal( + self, + remaining_packages_requirements: Dict[str, Set[str]], + all_conflicts: Dict[str, Set[str]], + to_remove: Set[str], + ) -> Dict[str, Set[str]]: + """Resolve the dependencies when a package is removed. + + Finds out (using recursive calls) all the packages that would need + to be removed together with the packages that want to be removed in + order not to leave broken dependencies. + + Args: + remaining_packages_requirements: The requirements of the + packages that would remain installed after uninstalling the + packages specified in `to_remove`. + all_conflicts: All the conflicts that accumulate as the + function is called recursively. At the end, a list of all + the packages that would need to be removed to leave the + system in a healthy state can be reconstructed from it. + to_remove: The packages that are to be removed. + """ + conflicts: Dict[str, Set[str]] = { + name: requirements & to_remove + for name, requirements in remaining_packages_requirements.items() + } + conflicts = { + name: conflicts + for name, conflicts in conflicts.items() if conflicts + } + all_conflicts.update(conflicts) + to_remove.update(conflicts) + remaining_packages_requirements = { + package: requirements + for package, requirements + in remaining_packages_requirements.items() + if package not in to_remove + } + if conflicts: + self._resolve_dependencies_removal( + remaining_packages_requirements, + all_conflicts, + to_remove + ) + return all_conflicts def _get_replaced_packages(self, new_packages): """Get package paths to install. @@ -213,8 +315,8 @@ def _install(self, files, filter_func, clear): for file in files_to_remove: os.remove(os.path.join(self.path, file)) if python_36: # Bound and unbound namespaces manually - import osp.core as core - import osp.core.namespaces as namespaces + from ... import core + from .. import namespaces if unbound_manually: unbound_manually = unbound_manually.difference( ns for ns in self.namespace_registry @@ -242,6 +344,25 @@ def _sort_for_installation(self, files, installed): requirements = {n: OntologyParser.get_parser(f).requirements for n, f in files.items()} + # If the requirements for an ontology package are bundled with + # OSP-core, try to install them automatically. + package_and_dependents = dict() + try: + package_and_dependents: Dict[str, Set[str]] = \ + self._resolve_dependencies_install( + files, requirements, dict() + ) + files.update({ + OntologyParser.get_parser(f).identifier: f + for f in package_and_dependents + }) + requirements.update({ + n: OntologyParser.get_parser(f).requirements + for n, f in files.items() + }) + except FileNotFoundError: + pass + # order the files while requirements: add_to_result = list() @@ -258,10 +379,93 @@ def _sort_for_installation(self, files, installed): result += add_to_result for x in add_to_result: del requirements[x] + dependencies_to_install = set(package_and_dependents) - set(installed) + if dependencies_to_install: + logger.info( + "Also installing dependencies: %s." + % ', '.join(dependencies_to_install) + ) logger.info("Will install the following namespaces: %s" % result) return [files[n] for n in result] + def _resolve_dependencies_install(self, + files: Dict[str, str], + requirements: Dict[str, Set[str]], + dependents: Dict[str, Set[str]]) -> \ + Dict[str, Set[str]]: + """Find and resolve the dependencies of the packages to be installed. + + Automatic resolution of dependencies is only feasible if the + dependency is bundled with OSP-core. + + Args: + files: The packages that are going to be installed together with + their file path. + requirements: The dependencies for each package that is going to + be installed. + dependents: A dictionary with package names and the packages + that depend on them. + """ + initial_files = files + additional_files: Dict[str, str] = dict() + # The statement below avoids installing the file bundled with + # OSP-core when the user provides a custom file providing the same + # package identifier. + requirements = { + n: {req for req in requirements_set + if req not in initial_files} + for n, requirements_set in requirements.items() + } + new_requirements: Dict[str, Set[str]] = dict() + for package, requirements_set in requirements.items(): + # Queue the requirements for installation if bundled with + # OSP-core and not already queued. + actually_missing_requirements = { + package + for package in requirements_set + if package not in additional_files + } + for requirement in actually_missing_requirements: + try: + parser = OntologyParser.get_parser(requirement) + additional_files[parser.identifier] = requirement + new_requirements.update({ + parser.identifier: parser.requirements + }) + except FileNotFoundError: + pass + + # Store which packages are requiring the requirements that were + # queued for installation to show the information on the logs. + # In addition, the `dependents` dictionary keys are the + # additional packages to be installed. + initially_missing_requirements = { + package + for package in requirements_set + if package not in initial_files + } + for requirement in initially_missing_requirements: + dependents[requirement] = dependents.get( + requirement, set()) | {package} + files.update(additional_files) + new_requirements_exist = bool( + {req + for req_set in new_requirements.values() + for req in req_set} + - {req + for req_set in requirements.values() + for req in req_set} + ) + if new_requirements_exist: + requirements.update(new_requirements) + dependents = self._resolve_dependencies_install( + files, + requirements, + dependents + ) + return dependents + def pico_migrate(namespace_registry, path): """Migrate old installations to new. diff --git a/osp/core/ontology/namespace.py b/osp/core/ontology/namespace.py index 17654983..ce064b46 100644 --- a/osp/core/ontology/namespace.py +++ b/osp/core/ontology/namespace.py @@ -273,7 +273,8 @@ def _iter_iris(self): """ types = [rdflib.OWL.DatatypeProperty, rdflib.OWL.ObjectProperty, - rdflib.OWL.Class] + rdflib.OWL.Class, + rdflib.RDFS.Class] return (s for t in types for s, _, _ in self._graph.triples((None, rdflib.RDF.type, t)) diff --git a/osp/core/ontology/namespace_registry.py b/osp/core/ontology/namespace_registry.py index 1451db2d..611e476b 100644 --- a/osp/core/ontology/namespace_registry.py +++ b/osp/core/ontology/namespace_registry.py @@ -146,7 +146,8 @@ def namespace_from_iri(self, ns_iri): def from_iri(self, iri, raise_error=True, allow_types=frozenset({rdflib.OWL.DatatypeProperty, rdflib.OWL.ObjectProperty, - rdflib.OWL.Class}), + rdflib.OWL.Class, + rdflib.RDFS.Class}), _name=None): """Get an entity from IRI. @@ -177,7 +178,7 @@ def from_iri(self, iri, raise_error=True, return OntologyAttribute(**kwargs) if o == rdflib.OWL.ObjectProperty: return OntologyRelationship(**kwargs) - if o == rdflib.OWL.Class: + if o in (rdflib.OWL.Class, rdflib.RDFS.Class): return OntologyClass(**kwargs) if raise_error: raise KeyError(f"IRI {iri} not found in graph or not of any " diff --git a/osp/core/ontology/parser/owl/parser.py b/osp/core/ontology/parser/owl/parser.py index 3e9753d4..ea24f920 100644 --- a/osp/core/ontology/parser/owl/parser.py +++ b/osp/core/ontology/parser/owl/parser.py @@ -3,20 +3,44 @@ import io import logging import os.path +from itertools import chain from typing import Dict, Set, Tuple, Optional -import rdflib import requests import yaml from rdflib import Graph, URIRef +from rdflib import OWL, RDF, RDFS from rdflib.util import guess_format import osp.core.ontology.parser.owl.keywords as keywords +import osp.core.warnings as warning_settings from osp.core.ontology.parser.parser import OntologyParser logger = logging.getLogger(__name__) +class RDFPropertiesWarning(UserWarning): + """Shown when an RDF file containing RDF properties is read. + + RDF properties are not supported by OSP-core, and therefore they are + ignored. This warning should not be shown when RDF properties are also + doubly defined as OWL object or data properties. + """ + + +class RDFPropertiesWarningFilter(logging.Filter): + """Attaches the `RDFPropertiesWarning` class to the records.""" + + def filter(self, record): + """Attaches the `RDFPropertiesWarning` to the records.""" + record.warning_class = RDFPropertiesWarning + return True + + +class EmptyOntologyFileError(RuntimeError): + """Should be raised when reading an ontology file with no entities.""" + + class OWLParser(OntologyParser): """Parses OWL ontologies.""" @@ -66,7 +90,7 @@ def requirements(self) -> Set[str]: @property def active_relationships(self) -> Tuple[URIRef]: """Fetch the active relationships from the ontology file.""" - return tuple(rdflib.URIRef(x) for x in + return tuple(URIRef(x) for x in self._yaml_config.get(keywords.ACTIVE_REL_KEY, tuple())) @property @@ -89,6 +113,7 @@ def graph(self) -> Graph: self._graph = self._read_ontology_graph(self._yaml_config, self._file_path, file_format) + self._validate_graph() return self._graph def __init__(self, path: str): @@ -216,3 +241,67 @@ def _read_ontology_graph(yaml_config_doc: dict, format=file_format) file_like.close() return graph + + def _validate_graph(self): + """Verify that the graph is an OWL ontology or RDFS vocabulary.""" + owl_entities = bool(next( + chain(*( + self._graph.subjects(RDF.type, type_) + for type_ in {OWL.Class, + OWL.DatatypeProperty, + OWL.ObjectProperty} + )), + False + )) # True when an OWL entity exists, False otherwise. + rdfs_classes = bool(next( + iter(self._graph.subjects(RDF.type, RDFS.Class)), + False + )) # True when an RDFS class exists, False otherwise. + + rdf_properties = set() + rdf_properties_count = 0 + max_rdf_properties_in_warning = 5 + for s in self._graph.subjects(RDF.type, RDF.Property): + has_owl_version = bool(next( + chain(*( + self._graph.subjects(RDF.type, type_) + for type_ in {OWL.DatatypeProperty, OWL.ObjectProperty} + )), + False + )) + if not has_owl_version: + if len(rdf_properties) < max_rdf_properties_in_warning: + rdf_properties.add(s) + rdf_properties_count += 1 + + if rdf_properties and warning_settings.rdf_properties_warning \ + in (True, None): + warning_text = ( + "The ontology package {package} contains the following RDF " + "properties: {properties}{more}. \n" + "As RDF properties are not supported by OSP-core, " + "the aforementioned properties will be ignored." + .format( + package=self.identifier, + properties=', '.join((str(identifier) + for identifier in rdf_properties)), + more=" and " + str(rdf_properties_count + - max_rdf_properties_in_warning) + + " more" if rdf_properties_count + > max_rdf_properties_in_warning else "")) + if warning_settings.rdf_properties_warning is not None: + warning_text += ( + "\n" + "You can turn off this warning running " + "`import osp.core.warnings as warning_settings; " + "warning_settings.rdf_property_warning = False`." + ) + logger_filter = RDFPropertiesWarningFilter() + logger.addFilter(logger_filter) + logger.warning(warning_text) + logger.removeFilter(logger_filter) + if not any((owl_entities, rdfs_classes)): + raise EmptyOntologyFileError( + f"No ontology entities detected in ontology package " + f"{self.identifier}. Are you sure it is an OWL ontology or an " + f"RDFS vocabulary?") diff --git a/osp/core/ontology/parser/yml/parser.py b/osp/core/ontology/parser/yml/parser.py index 27b7c05f..87fde071 100644 --- a/osp/core/ontology/parser/yml/parser.py +++ b/osp/core/ontology/parser/yml/parser.py @@ -183,7 +183,8 @@ def _add_type_triple(self, entity_name, iri): triple = (superclass_iri, RDF.type, None) for _, _, o in (ReadOnlyGraphAggregate( [self._graph, namespace_registry._graph]).triples(triple)): - if o in {OWL.Class, OWL.ObjectProperty, + if o in {OWL.Class, RDFS.Class, + OWL.ObjectProperty, OWL.DatatypeProperty, OWL.FunctionalProperty}: types.add(o) @@ -212,8 +213,10 @@ def _add_superclass(self, entity_name, iri, superclass_doc): superclass_iri = self._get_iri(superclass_name, namespace, entity_name) predicate = RDFS.subPropertyOf - if (iri, RDF.type, OWL.Class) in ReadOnlyGraphAggregate( - [self._graph, namespace_registry._graph]): + graph = ReadOnlyGraphAggregate([self._graph, + namespace_registry._graph]) + if ((iri, RDF.type, OWL.Class) in graph + or (iri, RDF.type, RDFS.Class) in graph): predicate = RDFS.subClassOf self._graph.add((iri, predicate, superclass_iri)) diff --git a/osp/core/pico.py b/osp/core/pico.py index 1dbbd33e..d7c0704a 100644 --- a/osp/core/pico.py +++ b/osp/core/pico.py @@ -6,6 +6,8 @@ from enum import Enum from typing import TYPE_CHECKING, Iterator +# import osp.core.warnings as warning_settings -> Not working with Python 3.6. +from . import warnings as warning_settings from osp.core.ontology.installation import OntologyInstallationManager if TYPE_CHECKING: @@ -93,6 +95,12 @@ def terminal(): args = parser.parse_args() logging.getLogger("osp.core").setLevel(getattr(logging, args.log_level)) + logging.getLogger("osp.core.ontology.installation")\ + .setLevel(getattr(logging, args.log_level)) + + # Force RDF properties warning when running from the terminal and do not + # offer the option to disable it. + warning_settings.rdf_properties_warning = None try: all_namespaces = map(lambda x: x.get_name(), diff --git a/osp/core/session/db/db_wrapper_session.py b/osp/core/session/db/db_wrapper_session.py index a85c34d0..8312d36f 100644 --- a/osp/core/session/db/db_wrapper_session.py +++ b/osp/core/session/db/db_wrapper_session.py @@ -168,7 +168,8 @@ def _is_cuds_iri_ontology(iri): .triples((rdflib.URIRef(iri), rdflib.RDF.type, None)): if o in frozenset({rdflib.OWL.DatatypeProperty, rdflib.OWL.ObjectProperty, - rdflib.OWL.Class}): + rdflib.OWL.Class, + rdflib.RDFS.Class}): return False return True diff --git a/osp/core/warnings.py b/osp/core/warnings.py index ac5ab84a..55c2c8b2 100644 --- a/osp/core/warnings.py +++ b/osp/core/warnings.py @@ -1,4 +1,5 @@ """Configuration of OSP-core warnings.""" +from typing import Union attributes_cannot_modify_in_place = True """Warns when a user fetches a mutable attribute of a CUDS object. @@ -17,3 +18,14 @@ The second parameter `unreachable_cuds_objects_large_dataset_size` controls the minimum size of a dataset needs to be in order to be considered large. """ + +rdf_properties_warning: Union[bool, None] = True +"""Warns when an RDF file containing RDF properties is read. + +RDF properties are not supported by OSP-core, and therefore they are +ignored. If the property is doubly defined also as an OWL data or object +property, then the warning is not emitted. + +When this warning setting is set to None, the option to disable it is not +mentioned within the warning. +""" diff --git a/tests/test_installation.py b/tests/test_installation.py index 753c95fd..c9e840f0 100644 --- a/tests/test_installation.py +++ b/tests/test_installation.py @@ -1,20 +1,25 @@ """Test the installation procedure.""" +import logging import os -import pathlib import shutil import tempfile +from pathlib import Path +from typing import Iterable, Tuple, Type, Union import unittest2 as unittest from rdflib import URIRef +import osp.core.warnings as warning_settings from osp.core.ontology.installation import OntologyInstallationManager, \ pico_migrate from osp.core.ontology.namespace_registry import NamespaceRegistry, \ namespace_registry -from osp.core.ontology.parser import Parser +from osp.core.ontology.parser.parser import Parser +from osp.core.ontology.parser.owl.parser import RDFPropertiesWarning, logger from osp.core.pico import install, namespaces, packages, uninstall + FILES = [ os.path.join(os.path.dirname(os.path.abspath(__file__)), "parser_test.yml"), @@ -22,10 +27,62 @@ "..", "osp", "core", "ontology", "docs", "city.ontology.yml"), ] +FILE_WITH_UNSATISFIABLE_REQUIREMENTS = f""" +identifier: parser_test +namespaces: + parser_test: http://www.osp-core.com/parser_test +ontology_file: {os.path.join(os.path.dirname(os.path.abspath(__file__)), + "parser_test.ttl")} +format: "ttl" +default_relationship: http://www.osp-core.com/parser_test#relationshipA +active_relationships: + - http://www.osp-core.com/parser_test#relationshipA + - http://www.osp-core.com/parser_test#relationshipB +requirements: + - fictional_package +""" + class TestInstallation(unittest.TestCase): """Test the installation procedure.""" + _rdf_file: tempfile.NamedTemporaryFile + """RDF file that does NOT contain an ontology.""" + + _yml_file: tempfile.NamedTemporaryFile + """YML configuration file for the previous file.""" + + @classmethod + def setUpClass(cls): + """Create additional ontology files.""" + cls._rdf_file = tempfile.NamedTemporaryFile(delete=False, + suffix='.ttl', + mode='w') + cls._rdf_file.write( + '@prefix ns1: .\n' + 'ns1:a ns1:meaningless ns1:triple .\n\n' + ) + cls._rdf_file.close() + cls._yml_file = tempfile.NamedTemporaryFile(delete=False, + suffix='.yml', + mode='w') + cls._yml_file.write( + f'identifier: test_pkg\n' + f'ontology_file: {cls._rdf_file.name}\n' + f'format: ttl\n' + f'reference_by_label: False\n' + f'namespaces: \n' + f' none: "none:"\n' + f'active_relationships: []\n' + ) + cls._yml_file.close() + + @classmethod + def tearDownClass(cls): + """Delete extra ontology files created during class setup.""" + Path(cls._rdf_file.name).unlink() + Path(cls._yml_file.name).unlink() + def setUp(self): """Set up some temporary directories.""" self.tempdir = tempfile.TemporaryDirectory() @@ -170,8 +227,14 @@ def test_sort_for_installation(self): r = self.installer._sort_for_installation( ["parser_test", "city"], set()) self.assertEqual(r, ["city", "parser_test"]) - self.assertRaises(RuntimeError, self.installer._sort_for_installation, - ["parser_test"], set()) + + # Test unsatisfiable requirements + with tempfile.TemporaryDirectory(): + with open('ontology_file.yml', 'w') as file: + file.write(FILE_WITH_UNSATISFIABLE_REQUIREMENTS) + self.assertRaises(RuntimeError, + self.installer._sort_for_installation, + [file.name], set()) def test_pico_migrate(self): """Test migration of installed ontologies.""" @@ -188,6 +251,65 @@ def test_pico_migrate(self): self.assertEqual(sorted(os.listdir(path)), sorted([ 'city.yml', 'graph.xml', 'namespaces.txt'])) + def test_empty_file(self): + """Tests installing an RDF file NOT containing an ontology.""" + def install_empty_file(): + """Install an RDF file that contains no ontology information.""" + self.installer._install([self._yml_file.name], + lambda x: (x for x in x), + clear=False) + + self.assertRaises(RuntimeError, install_empty_file) + + def test_dcterms(self): + """Test DCMI Metadata Terms installation.""" + def count_warnings_by_class(records: Iterable[logging.LogRecord], + classes: Union[Type, Tuple[Type, ...]]) \ + -> int: + """Given log records, count their "classes" if attached. + + For each record, checks if it has a `warning_class` attribute, + and checks whether its value is a subclass of the classes + provided. + """ + return sum( + bool(issubclass(record.warning_class, classes) + if hasattr(record, 'warning_class') else False) + for record in records + ) + + original_warning_setting = warning_settings.rdf_properties_warning + try: + warning_settings.rdf_properties_warning = False + with self.assertLogs(logger=logger) as captured: + logger.warning('At least one log entry is needed for ' + '`assertLogs`.') + self.installer._install(['dcterms', 'dcmitype'], + lambda x: (x for x in x), + clear=True) + self.assertEqual( + count_warnings_by_class( + captured.records, + (RDFPropertiesWarning, )), + 0 + ) + + warning_settings.rdf_properties_warning = True + with self.assertLogs(logger=logger) as captured: + logger.warning('At least one log entry is needed for ' + '`assertLogs`.') + self.installer._install(['dcterms', 'dcmitype'], + lambda x: (x for x in x), + clear=True) + self.assertEqual( + count_warnings_by_class( + captured.records, + (RDFPropertiesWarning, )), + 1 # dcmi-type has no properties + ) + finally: + warning_settings.rdf_properties_warning = original_warning_setting + class PicoModule(unittest.TestCase): """Test the use of pico as a Python module.""" @@ -197,7 +319,7 @@ def setUp(self) -> None: self._previous_installation_path = OntologyInstallationManager\ .get_default_installation_path() self._new_installation_path = \ - pathlib.Path('.TEST_OSP_CORE_INSTALLATION').absolute() + Path('.TEST_OSP_CORE_INSTALLATION').absolute() os.makedirs(self._new_installation_path / '.osp_ontologies', exist_ok=True) OntologyInstallationManager.set_default_installation_path( @@ -209,7 +331,7 @@ def setUp(self) -> None: def tearDown(self) -> None: """Revert changes done during the execution of the `setUp` method.""" OntologyInstallationManager.set_default_installation_path( - str(pathlib.Path(self._previous_installation_path).parent)) + str(Path(self._previous_installation_path).parent)) shutil.rmtree(self._new_installation_path) namespace_registry.clear() namespace_registry.load_graph_file( diff --git a/tests/test_parser.py b/tests/test_parser.py index 317733b0..a58de25e 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -13,8 +13,8 @@ from rdflib.compare import isomorphic from osp.core.ontology.namespace_registry import NamespaceRegistry -from osp.core.ontology.parser import Parser -from osp.core.ontology.parser.parser import OntologyParser +from osp.core.ontology.parser.owl.parser import EmptyOntologyFileError +from osp.core.ontology.parser.parser import OntologyParser, Parser RDF_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), @@ -129,9 +129,12 @@ def request_callback(request): yml_path = os.path.join(tempdir, 'parser_test.yml') with open(yml_path, 'w') as file: yaml.safe_dump(doc, file) - parser = OntologyParser.get_parser(yml_path) - self.assertIn((rdflib.URIRef("ns1:a"), rdflib.URIRef("ns1:b"), - rdflib.URIRef("ns1:c")), parser.graph) + try: + parser = OntologyParser.get_parser(yml_path) + self.assertIn((rdflib.URIRef("ns1:a"), rdflib.URIRef("ns1:b"), + rdflib.URIRef("ns1:c")), parser.graph) + except EmptyOntologyFileError: + pass def test_get_file_path(self): """Test the get_file_path method."""