From 0e5ac7d5613f6bd8c9eaf298d5e9d83e2e729e5d Mon Sep 17 00:00:00 2001 From: Casper Welzel Andersen Date: Thu, 23 Sep 2021 17:24:35 +0200 Subject: [PATCH] First tests for dic2owl.Generator class Implement better typing all-round. --- dic2owl/dic2owl/__init__.py | 3 + dic2owl/dic2owl/dic2owl.py | 42 ++++++----- tests/dic2owl/conftest.py | 66 ++++++++++++++---- tests/dic2owl/test_cli.py | 34 +++++---- tests/dic2owl/test_dic2owl_generator.py | 92 +++++++++++++++++++++++++ 5 files changed, 187 insertions(+), 50 deletions(-) create mode 100644 tests/dic2owl/test_dic2owl_generator.py diff --git a/dic2owl/dic2owl/__init__.py b/dic2owl/dic2owl/__init__.py index cbde23a..45a8fe2 100644 --- a/dic2owl/dic2owl/__init__.py +++ b/dic2owl/dic2owl/__init__.py @@ -10,7 +10,10 @@ terminal. """ # pylint: disable=line-too-long +from .dic2owl import Generator __version__ = "0.2.0" __author__ = "Jesper Friis , Casper Welzel Andersen , Francesca Lønstad Bleken " __author_email__ = "cif@emmo-repo.eu" + +__all__ = ("Generator",) diff --git a/dic2owl/dic2owl/dic2owl.py b/dic2owl/dic2owl/dic2owl.py index 759552c..d4960d0 100644 --- a/dic2owl/dic2owl/dic2owl.py +++ b/dic2owl/dic2owl/dic2owl.py @@ -9,7 +9,7 @@ # import textwrap import types -from typing import Any, Set, Union, Sequence +from typing import TYPE_CHECKING import urllib.request from CifFile import CifDic @@ -19,24 +19,29 @@ with open(DEVNULL, "w") as handle: # pylint: disable=unspecified-encoding with redirect_stderr(handle): from emmo import World - from emmo.ontology import Ontology from owlready2 import locstr +if TYPE_CHECKING: + from _typeshed import StrPath + from typing import Any, Sequence, Set, Union + + from emmo.ontology import Ontology + # Workaround for flaw in EMMO-Python # To be removed when EMMO-Python doesn't requires ontologies to import SKOS -import emmo.ontology # noqa: E402 +import emmo.ontology # pylint: disable=wrong-import-position emmo.ontology.DEFAULT_LABEL_ANNOTATIONS = [ "http://www.w3.org/2000/01/rdf-schema#label", ] -"""The absolute, normalized path to the `ontology` directory in this -repository""" ONTOLOGY_DIR = ( Path(__file__).resolve().parent.parent.parent.joinpath("ontology") ) +"""The absolute, normalized path to the `ontology` directory in this +repository""" def lang_en(string: str) -> locstr: @@ -67,18 +72,23 @@ class Generator: """ + CIF_DDL = ( + "https://raw.githubusercontent.com/emmo-repo/CIF-ontology/main/" + "ontology/cif-ddl.ttl" + ) + # TODO: # Should `comments` be replaced with a dict `annotations` for annotating # the ontology itself? If so, we should import Dublin Core. def __init__( self, - dicfile: str, + dicfile: "StrPath", base_iri: str, - comments: Sequence[str] = (), + comments: "Sequence[str]" = (), ) -> None: self.dicfile = dicfile - self.dic = CifDic(dicfile, do_dREL=False) + self.dic = CifDic(str(self.dicfile), do_dREL=False) self.comments = comments # Create new ontology @@ -86,11 +96,7 @@ def __init__( self.onto = self.world.get_ontology(base_iri) # Load cif-ddl ontology and append it to imported ontologies - cif_ddl = ( - "https://raw.githubusercontent.com/emmo-repo/CIF-ontology/main/" - "ontology/cif-ddl.ttl" - ) - self.ddl = self.world.get_ontology(str(cif_ddl)).load() + self.ddl = self.world.get_ontology(self.CIF_DDL).load() self.ddl.sync_python_names() self.onto.imported_ontologies.append(self.ddl) @@ -98,9 +104,9 @@ def __init__( # dcterms = self.world.get_ontology('http://purl.org/dc/terms/').load() # self.onto.imported_ontologies.append(dcterms) - self.items: Set[dict] = set() + self.items: "Set[dict]" = set() - def generate(self) -> Ontology: + def generate(self) -> "Ontology": """Generate ontology for the CIF dictionary. Returns: @@ -194,7 +200,7 @@ def _add_data_value(self, item: dict) -> None: self._add_annotations(cls, item) - def _add_annotations(self, cls: Any, item: dict) -> None: + def _add_annotations(self, cls: "Any", item: dict) -> None: """Add annotations for dic item `item` to generated ontology class `cls`. @@ -228,7 +234,9 @@ def _add_metadata(self) -> None: ) -def main(dicfile: Union[str, Path], ttlfile: Union[str, Path]) -> Generator: +def main( + dicfile: "Union[str, Path]", ttlfile: "Union[str, Path]" +) -> Generator: """Main function for ontology generation. Parameters: diff --git a/tests/dic2owl/conftest.py b/tests/dic2owl/conftest.py index f4c06d6..54f2b4f 100644 --- a/tests/dic2owl/conftest.py +++ b/tests/dic2owl/conftest.py @@ -3,7 +3,7 @@ # pylint: disable=redefined-outer-name,inconsistent-return-statements from enum import Enum from pathlib import Path -from typing import List, Optional, TYPE_CHECKING, Union +from typing import TYPE_CHECKING import pytest @@ -17,7 +17,7 @@ class CLI(Enum): if TYPE_CHECKING: from subprocess import CompletedProcess - from typing import Callable + from typing import Callable, List, Optional, Union CLIRunner = Callable[ [ @@ -32,17 +32,17 @@ class CLI(Enum): @pytest.fixture(scope="session") def clirunner() -> "CLIRunner": - """Call `dic2owl` CLI""" + """Call a CLI""" from subprocess import run, CalledProcessError from tempfile import TemporaryDirectory def _clirunner( - options: Optional[List[str]] = None, - cli: Optional[Union[CLI, str]] = None, - expected_error: Optional[str] = None, - run_dir: Optional[Union[Path, str]] = None, - ) -> Union["CompletedProcess", CalledProcessError]: - """Call `dic2owl` CLI + options: "Optional[List[str]]" = None, + cli: "Optional[Union[CLI, str]]" = None, + expected_error: "Optional[str]" = None, + run_dir: "Optional[Union[Path, str]]" = None, + ) -> "Union[CompletedProcess, CalledProcessError]": + """Call a CLI Parameters: options: Options with which to call `cli`, e.g., `--version`. @@ -140,11 +140,47 @@ def cif_ttl(top_dir: Path) -> str: """Read and return CIF-Core minimized Turtle file (generated from the accompanying dictionary). - NOTE: The commend conerning the file location has been removed manually + NOTE: The comment conerning the file location has been removed manually from this file. """ - with open( - top_dir / "tests/dic2owl/static/cif_core_minimized.ttl", "r" - ) as handle: - content = handle.read() - return content + return ( + top_dir / "tests/dic2owl/static/cif_core_minimized.ttl" + ).read_text() + + +@pytest.fixture(scope="session") +def base_iri() -> str: + """Return standard CIF-Core base IRI.""" + return "http://emmo.info/CIF-ontology/ontology/cif_core#" + + +@pytest.fixture(scope="session") +def cif_dic_path(top_dir: Path) -> Path: + """Return path to minimized CIF-Core dictionary.""" + return top_dir / "tests" / "dic2owl" / "static" / "cif_core_minimized.dic" + + +@pytest.fixture +def create_location_free_ttl() -> "Callable[[Path], str]": + """Create file location comment-free turtle file.""" + + def _create_location_free_ttl(ttlfile: Path) -> str: + """Create file location comment-free turtle file. + + Parameters: + ttlfile: Path to turtle file. + + Returns: + Content of turtle file without the file location line. + """ + generated_ttl = "" + with ttlfile.open() as handle: + for line in handle.readlines(): + if "dic2owl" in line: + # Skip comment line concerning the file location + pass + else: + generated_ttl += line + return generated_ttl + + return _create_location_free_ttl diff --git a/tests/dic2owl/test_cli.py b/tests/dic2owl/test_cli.py index c9967b1..44f9b82 100644 --- a/tests/dic2owl/test_cli.py +++ b/tests/dic2owl/test_cli.py @@ -4,6 +4,8 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: + from typing import Callable + from .conftest import CLIRunner @@ -16,7 +18,10 @@ def test_version(clirunner: "CLIRunner") -> None: def test_local_file( - clirunner: "CLIRunner", top_dir: Path, cif_ttl: str + clirunner: "CLIRunner", + top_dir: Path, + cif_ttl: str, + create_location_free_ttl: "Callable[[Path], str]", ) -> None: """Test a normal/default run with minimum input. @@ -35,14 +40,9 @@ def test_local_file( "downloading" in output.stdout ), f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" - generated_ttl = "" - with open(Path(tmpdir) / "cif_core_minimized.ttl", "r") as handle: - for line in handle.readlines(): - if "dic2owl" in line: - # Skip comment line concerning the file location - pass - else: - generated_ttl += line + generated_ttl = create_location_free_ttl( + Path(tmpdir) / "cif_core_minimized.ttl" + ) assert generated_ttl == cif_ttl @@ -54,7 +54,12 @@ def test_local_file( ) -def test_output(clirunner: "CLIRunner", top_dir: Path, cif_ttl: str) -> None: +def test_output( + clirunner: "CLIRunner", + top_dir: Path, + cif_ttl: str, + create_location_free_ttl: "Callable[[Path], str]", +) -> None: """Test `--output`. NOTE: The commend conerning the file location has been removed from the @@ -75,14 +80,7 @@ def test_output(clirunner: "CLIRunner", top_dir: Path, cif_ttl: str) -> None: "downloading" in output.stdout ), f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" - generated_ttl = "" - with open(out_ttl, "r") as handle: - for line in handle.readlines(): - if "dic2owl" in line: - # Skip comment line concerning the file location - pass - else: - generated_ttl += line + generated_ttl = create_location_free_ttl(Path(out_ttl)) assert generated_ttl == cif_ttl diff --git a/tests/dic2owl/test_dic2owl_generator.py b/tests/dic2owl/test_dic2owl_generator.py new file mode 100644 index 0000000..5015bde --- /dev/null +++ b/tests/dic2owl/test_dic2owl_generator.py @@ -0,0 +1,92 @@ +"""Test the `dic2owl.dic2owl.Generator` class.""" +# pylint: disable=redefined-outer-name,import-outside-toplevel +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from typing import Callable, List, Optional + + from dic2owl import Generator + + +@pytest.fixture(scope="session") +def sample_generator_comments() -> "List[str]": + """The comments to be used for the `sample_generator` fixture.""" + return ["This is a test."] + + +@pytest.fixture +def sample_generator( + base_iri: str, cif_dic_path: Path, sample_generator_comments: "List[str]" +) -> "Callable[[Optional[List[str]]], Generator]": + """Create a generator similar to what is tested in + `test_initialization()`.""" + from dic2owl import Generator + + def _sample_generator(comments: "Optional[List[str]]" = None) -> Generator: + """Create and return a `Generator` with specific list of metadata + comments. By default, the fixture `sample_generator_comments` is + used.""" + return Generator( + dicfile=cif_dic_path, + base_iri=base_iri, + comments=sample_generator_comments + if comments is None + else comments, + ) + + return _sample_generator + + +def test_initialization( + base_iri: str, cif_dic_path: Path, sample_generator_comments: "List[str]" +) -> None: + """Ensure a newly initialized Generator has intended ontologies and + properties.""" + from CifFile import CifDic + from dic2owl import Generator + + cif_dictionary = CifDic(str(cif_dic_path), do_dREL=False) + + generator = Generator( + dicfile=cif_dic_path, + base_iri=base_iri, + comments=sample_generator_comments, + ) + + assert generator + assert generator.dic.WriteOut() == cif_dictionary.WriteOut() + assert generator.ddl + assert generator.ddl in generator.onto.imported_ontologies + assert generator.comments == sample_generator_comments + + +def test_generate( + cif_ttl: str, + create_location_free_ttl: "Callable[[Path], str]", + sample_generator: "Callable[[Optional[List[str]]], Generator]", + sample_generator_comments: "List[str]", +) -> None: + """Test the `generate()` method.""" + from tempfile import NamedTemporaryFile + + generator = sample_generator(None) + generated_ontology = generator.generate() + + for comment in sample_generator_comments: + assert comment in generated_ontology.metadata.comment + assert ( + f"Generated with dic2owl from {generator.dicfile}" + in generated_ontology.metadata.comment + ) + + generated_ontology = sample_generator([]).generate() + + with NamedTemporaryFile() as output_turtle: + generated_ontology.save(output_turtle.name, format="turtle") + + generated_ttl = create_location_free_ttl(Path(output_turtle.name)) + + assert generated_ttl == cif_ttl