diff --git a/.github/workflows/dic2owl_ci.yml b/.github/workflows/dic2owl_ci.yml index 0e39b03..798ca1a 100644 --- a/.github/workflows/dic2owl_ci.yml +++ b/.github/workflows/dic2owl_ci.yml @@ -4,6 +4,8 @@ on: push: paths: - 'dic2owl/**' + - 'tests/**' + - '.github/**' jobs: @@ -41,5 +43,5 @@ jobs: - name: Lint with MyPy run: mypy --ignore-missing-imports --scripts-are-modules dic2owl/dic2owl - # - name: Run unittests - # run: pytest --cov dic2owl/dic2owl tests/ + - name: Run unit tests with PyTest + run: pytest --cov dic2owl tests/ diff --git a/.gitignore b/.gitignore index bc1ddcd..5866066 100644 --- a/.gitignore +++ b/.gitignore @@ -5,8 +5,10 @@ __pycache__ *.pyc .mypy* +.coverage *.dic *.cif !test-cif/**/*.cif +!tests/**/*.dic diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e895e6d..ee1b286 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,14 +7,14 @@ repos: exclude: .*(\.md|\.ttl|\.cif)$ - repo: https://github.com/ambv/black - rev: 21.7b0 + rev: 21.11b1 hooks: - id: black args: - --config=dic2owl/pyproject.toml - repo: https://github.com/pycqa/pylint - rev: 'v2.10.1' + rev: 'v2.12.1' hooks: - id: pylint args: 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/cli.py b/dic2owl/dic2owl/cli.py index 2277c93..b5b77fc 100644 --- a/dic2owl/dic2owl/cli.py +++ b/dic2owl/dic2owl/cli.py @@ -5,13 +5,14 @@ ontology-generation tool for CIF `.dic`-files. """ import argparse -import logging + +# import logging from pathlib import Path -LOGGING_LEVELS = [ - logging.getLevelName(level).lower() for level in range(0, 51, 10) -] +# LOGGING_LEVELS = [ +# logging.getLevelName(level).lower() for level in range(0, 51, 10) +# ] def main(argv: list = None) -> None: @@ -33,18 +34,18 @@ def main(argv: list = None) -> None: help="Show the version and exit.", version=f"dic2owl version {__version__}", ) - parser.add_argument( - "--log-level", - type=str, - help="Set the stdout log-level (verbosity).", - choices=LOGGING_LEVELS, - default="info", - ) - parser.add_argument( - "--debug", - action="store_true", - help="Overrule log-level option, setting it to 'debug'.", - ) + # parser.add_argument( + # "--log-level", + # type=str, + # help="Set the stdout log-level (verbosity).", + # choices=LOGGING_LEVELS, + # default="info", + # ) + # parser.add_argument( + # "--debug", + # action="store_true", + # help="Overrule log-level option, setting it to 'debug'.", + # ) parser.add_argument( "-o", "--output", 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/dic2owl/requirements_dev.txt b/dic2owl/requirements_dev.txt index 84a2750..09b7b96 100644 --- a/dic2owl/requirements_dev.txt +++ b/dic2owl/requirements_dev.txt @@ -2,3 +2,5 @@ bandit~=1.7.0 mypy==0.910 pre-commit~=2.15 pylint~=2.11 +pytest~=6.2 +pytest-cov~=2.12 diff --git a/tests/dic2owl/conftest.py b/tests/dic2owl/conftest.py new file mode 100644 index 0000000..df3efde --- /dev/null +++ b/tests/dic2owl/conftest.py @@ -0,0 +1,261 @@ +"""PyTest fixtures for `dic2owl`.""" +# pylint: disable=import-outside-toplevel,consider-using-with,too-many-branches +# pylint: disable=redefined-outer-name,inconsistent-return-statements +# pylint: disable=too-many-statements +from collections import namedtuple +from enum import Enum +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + + +class CLI(Enum): + """Enumeration of CLIs.""" + + CIF2OWL = "cif2owl" + DIC2OWL = "dic2owl" + + +if TYPE_CHECKING: + from subprocess import CompletedProcess + from typing import Callable, List, Optional, Union + + CLIRunner = Callable[ + [ + Optional[List[str]], + Optional[Union[CLI, str]], + Optional[str], + Optional[Union[Path, str]], + bool, + ], + CompletedProcess, + ] + + +@pytest.fixture(scope="session") +def clirunner() -> "CLIRunner": + """Call a CLI""" + from contextlib import redirect_stderr, redirect_stdout + import importlib + import os + from subprocess import run, CalledProcessError + from tempfile import TemporaryDirectory + + CLIOutput = namedtuple("CLIOutput", ["stdout", "stderr"]) + + 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, + use_subprocess: bool = True, + ) -> "Union[CompletedProcess, CalledProcessError, CLIOutput]": + """Call a CLI + + Parameters: + options: Options with which to call `cli`, e.g., `--version`. + cli: The CLI to call, defaults to `dic2owl`. + expected_error: Sub-string expected in error output, if an error is + expected. + run_dir: The directory to use as current work directory when + running the CLI. + use_subprocess: Whether or not to run the CLI through a + `subprocess.run()` call or instead import and call + `dic2owl.cli.main()` directly. + + Returns: + The return class for a successful call to `subprocess.run()` or the + captured response from importing and running the `main()` function + directly. + + """ + options = options or [] + + if not isinstance(options, list): + try: + options = list(options) + except TypeError as exc: + raise TypeError("options must be a list of strings.") from exc + + if cli is not None: + try: + cli = CLI(cli) + except ValueError as exc: + raise ValueError( + f"{cli!r} is not a recognized CLI. Recognized CLIs: " + f"{list(CLI.__members__)}" + ) from exc + else: + cli = CLI.DIC2OWL + + if run_dir is None: + run_dir = TemporaryDirectory() + elif isinstance(run_dir, Path): + run_dir = run_dir.resolve() + else: + try: + run_dir = Path(run_dir).resolve() + except TypeError as exc: + raise TypeError(f"{run_dir} is not a valid path.") from exc + + if use_subprocess: + try: + output = run( + args=[cli.value] + options, + capture_output=True, + check=True, + cwd=run_dir.name + if isinstance(run_dir, TemporaryDirectory) + else run_dir, + text=True, + ) + if expected_error: + pytest.fail( + "Expected the CLI call to fail with an error " + f"containing the sub-string: {expected_error}" + ) + except CalledProcessError as error: + if expected_error: + if ( + expected_error in error.stdout + or expected_error in error.stderr + ): + # Expected error, found expected sub-string as well. + return error + + pytest.fail( + "The CLI call failed as expected, but the expected " + "error sub-string could not be found in stdout or " + f"stderr. Sub-string: {expected_error}\nSTDOUT: " + f"{error.stdout}\nSTDERR: {error.stderr}" + ) + else: + pytest.fail( + "The CLI call failed when it didn't expect to.\n" + f"STDOUT: {error.stdout}\nSTDERR: {error.stderr}" + ) + else: + return output + finally: + if isinstance(run_dir, TemporaryDirectory): + run_dir.cleanup() + else: + cli_name = importlib.import_module(f"{cli.value}.cli") + + with TemporaryDirectory() as tmpdir: + stdout_path = Path(tmpdir) / "out.txt" + stderr_path = Path(tmpdir) / "err.txt" + original_cwd = os.getcwd() + try: + os.chdir( + run_dir.name + if isinstance(run_dir, TemporaryDirectory) + else run_dir + ) + with open(stdout_path, "w") as stdout, open( + stderr_path, "w" + ) as stderr: + with redirect_stdout(stdout), redirect_stderr(stderr): + cli_name.main(options if options else None) + output = CLIOutput( + stdout_path.read_text(), stderr_path.read_text() + ) + except SystemExit as exc: + output = CLIOutput( + stdout_path.read_text(), stderr_path.read_text() + ) + if str(exc) != "0": + pytest.fail( + "The CLI call failed when it didn't expect to.\n" + f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" + ) + return output + except Exception: # pylint: disable=broad-except + output = CLIOutput( + stdout_path.read_text(), stderr_path.read_text() + ) + if expected_error: + if ( + expected_error in output.stdout + or expected_error in output.stderr + ): + # Expected error, found expected sub-string as well. + return output + + pytest.fail( + "The CLI call failed as expected, but the expected " + "error sub-string could not be found in stdout or " + f"stderr. Sub-string: {expected_error}\nSTDOUT: " + f"{output.stdout}\nSTDERR: {output.stderr}" + ) + else: + pytest.fail( + "The CLI call failed when it didn't expect to.\n" + f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" + ) + else: + return output + finally: + os.chdir(original_cwd) + if isinstance(run_dir, TemporaryDirectory): + run_dir.cleanup() + + return _clirunner + + +@pytest.fixture(scope="session") +def top_dir() -> Path: + """Return repository path.""" + return Path(__file__).parent.parent.parent.resolve() + + +@pytest.fixture(scope="session") +def cif_ttl(top_dir: Path) -> str: + """Read and return CIF-Core minimized Turtle file (generated from the + accompanying dictionary). + + NOTE: The comment conerning the file location has been removed manually + from this file. + """ + 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/static/cif_core_minimized.dic b/tests/dic2owl/static/cif_core_minimized.dic new file mode 100755 index 0000000..12e1d58 --- /dev/null +++ b/tests/dic2owl/static/cif_core_minimized.dic @@ -0,0 +1,185 @@ +#\#CIF_2.0 +########################################################################## +# # +# CIF CORE DICTIONARY # +# # +########################################################################## + +data_CORE_DIC + + _dictionary.title CORE_DIC + _dictionary.class Instance + _dictionary.version 3.1.0 + _dictionary.date 2021-08-18 + _dictionary.uri + https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic + _dictionary.ddl_conformance 4.0.1 + _dictionary.namespace CifCore + _description.text +; + The CIF_CORE dictionary records all the CORE data items defined + and used with in the Crystallographic Information Framework (CIF). +; + +save_CIF_CORE + + _definition.id CIF_CORE + _definition.scope Category + _definition.class Head + _definition.update 2014-06-18 + _description.text +; + The CIF_CORE group contains the definitions of data items that + are common to all domains of crystallographic studies. +; + _name.category_id CORE_DIC + _name.object_id CIF_CORE + +save_ + +save_DIFFRACTION + + _definition.id DIFFRACTION + _definition.scope Category + _definition.class Set + _definition.update 2012-11-26 + _description.text +; + The DICTIONARY group encompassing the CORE DIFFRACTION data items defined + and used with in the Crystallographic Information Framework (CIF). +; + _name.category_id CIF_CORE + _name.object_id DIFFRACTION + +save_ + +save_DIFFRN + + _definition.id DIFFRN + _definition.scope Category + _definition.class Set + _definition.update 2012-12-13 + _description.text +; + The CATEGORY of data items used to describe the diffraction experiment. +; + _name.category_id DIFFRACTION + _name.object_id DIFFRN + +save_ + +save_diffrn.ambient_environment + + _definition.id '_diffrn.ambient_environment' + _alias.definition_id '_diffrn_ambient_environment' + _definition.update 2012-11-26 + _description.text +; + The gas or liquid environment of the crystal sample, if not air. +; + _name.category_id diffrn + _name.object_id ambient_environment + _type.purpose Describe + _type.source Recorded + _type.container Single + _type.contents Text + + loop_ + _description_example.case + 'He' + 'vacuum' + 'mother liquor' + +save_ + +save_diffrn.ambient_pressure + + _definition.id '_diffrn.ambient_pressure' + _alias.definition_id '_diffrn_ambient_pressure' + _definition.update 2012-11-26 + _description.text +; + Mean hydrostatic pressure at which intensities were measured. +; + _name.category_id diffrn + _name.object_id ambient_pressure + _type.purpose Measurand + _type.source Recorded + _type.container Single + _type.contents Real + _enumeration.range 0.0: + _units.code kilopascals + +save_ + +save_diffrn.ambient_pressure_su + + _definition.id '_diffrn.ambient_pressure_su' + + loop_ + _alias.definition_id + '_diffrn_ambient_pressure_su' + '_diffrn.ambient_pressure_esd' + + _definition.update 2021-03-03 + _description.text +; + Standard uncertainty of the mean hydrostatic pressure + at which intensities were measured. +; + _name.category_id diffrn + _name.object_id ambient_pressure_su + _name.linked_item_id '_diffrn.ambient_pressure' + _type.purpose SU + _type.source Recorded + _type.container Single + _type.contents Real + _units.code kilopascals + +save_ + +save_diffrn.ambient_pressure_gt + + _definition.id '_diffrn.ambient_pressure_gt' + _alias.definition_id '_diffrn_ambient_pressure_gt' + _definition.update 2012-12-13 + _description.text +; + Mean hydrostatic pressure above which intensities were measured. + These items allow for a pressure range to be given. + _diffrn.ambient_pressure should be used in preference to this + item when possible. +; + _name.category_id diffrn + _name.object_id ambient_pressure_gt + _type.purpose Number + _type.source Recorded + _type.container Single + _type.contents Real + _enumeration.range 0.0: + _units.code kilopascals + +save_ + +save_diffrn.ambient_pressure_lt + + _definition.id '_diffrn.ambient_pressure_lt' + _alias.definition_id '_diffrn_ambient_pressure_lt' + _definition.update 2012-12-13 + _description.text +; + Mean hydrostatic pressure below which intensities were measured. + These items allow for a pressure range to be given. + _diffrn.ambient_pressure should be used in preference to this + item when possible. +; + _name.category_id diffrn + _name.object_id ambient_pressure_lt + _type.purpose Number + _type.source Recorded + _type.container Single + _type.contents Real + _enumeration.range 0.0: + _units.code kilopascals + +save_ diff --git a/tests/dic2owl/static/cif_core_minimized.ttl b/tests/dic2owl/static/cif_core_minimized.ttl new file mode 100644 index 0000000..b9ded87 --- /dev/null +++ b/tests/dic2owl/static/cif_core_minimized.ttl @@ -0,0 +1,178 @@ +@prefix : . +@prefix cif-: . +@prefix owl: . +@prefix rdf: . +@prefix rdfs: . +@prefix xml: . +@prefix xsd: . + + a owl:Ontology ; + owl:imports . + +:_diffrn.ambient_environment a owl:Class ; + :prefLabel "_diffrn.ambient_environment"@en ; + cif-:_alias.definition_id "_diffrn_ambient_environment"@en ; + cif-:_definition.id "_diffrn.ambient_environment"@en ; + cif-:_definition.update "2012-11-26"@en ; + cif-:_description.text """ + The gas or liquid environment of the crystal sample, if not air."""@en ; + cif-:_description_example.case "['He', 'vacuum', 'mother liquor']"@en ; + cif-:_name.category_id "diffrn"@en ; + cif-:_name.object_id "ambient_environment"@en ; + cif-:_type.container "Single"@en ; + cif-:_type.contents "Text"@en ; + cif-:_type.purpose "Describe"@en ; + cif-:_type.source "Recorded"@en ; + rdfs:subClassOf :DIFFRN, + cif-:Describe, + cif-:Recorded, + cif-:Single, + cif-:Text . + +:_diffrn.ambient_pressure a owl:Class ; + :prefLabel "_diffrn.ambient_pressure"@en ; + cif-:_alias.definition_id "_diffrn_ambient_pressure"@en ; + cif-:_definition.id "_diffrn.ambient_pressure"@en ; + cif-:_definition.update "2012-11-26"@en ; + cif-:_description.text """ + Mean hydrostatic pressure at which intensities were measured."""@en ; + cif-:_enumeration.range "0.0:"@en ; + cif-:_name.category_id "diffrn"@en ; + cif-:_name.object_id "ambient_pressure"@en ; + cif-:_type.container "Single"@en ; + cif-:_type.contents "Real"@en ; + cif-:_type.purpose "Measurand"@en ; + cif-:_type.source "Recorded"@en ; + cif-:_units.code "kilopascals"@en ; + rdfs:subClassOf :DIFFRN, + cif-:Measurand, + cif-:Real, + cif-:Recorded, + cif-:Single . + +:_diffrn.ambient_pressure_gt a owl:Class ; + :prefLabel "_diffrn.ambient_pressure_gt"@en ; + cif-:_alias.definition_id "_diffrn_ambient_pressure_gt"@en ; + cif-:_definition.id "_diffrn.ambient_pressure_gt"@en ; + cif-:_definition.update "2012-12-13"@en ; + cif-:_description.text """ + Mean hydrostatic pressure above which intensities were measured. + These items allow for a pressure range to be given. + _diffrn.ambient_pressure should be used in preference to this + item when possible."""@en ; + cif-:_enumeration.range "0.0:"@en ; + cif-:_name.category_id "diffrn"@en ; + cif-:_name.object_id "ambient_pressure_gt"@en ; + cif-:_type.container "Single"@en ; + cif-:_type.contents "Real"@en ; + cif-:_type.purpose "Number"@en ; + cif-:_type.source "Recorded"@en ; + cif-:_units.code "kilopascals"@en ; + rdfs:subClassOf :DIFFRN, + cif-:Number, + cif-:Real, + cif-:Recorded, + cif-:Single . + +:_diffrn.ambient_pressure_lt a owl:Class ; + :prefLabel "_diffrn.ambient_pressure_lt"@en ; + cif-:_alias.definition_id "_diffrn_ambient_pressure_lt"@en ; + cif-:_definition.id "_diffrn.ambient_pressure_lt"@en ; + cif-:_definition.update "2012-12-13"@en ; + cif-:_description.text """ + Mean hydrostatic pressure below which intensities were measured. + These items allow for a pressure range to be given. + _diffrn.ambient_pressure should be used in preference to this + item when possible."""@en ; + cif-:_enumeration.range "0.0:"@en ; + cif-:_name.category_id "diffrn"@en ; + cif-:_name.object_id "ambient_pressure_lt"@en ; + cif-:_type.container "Single"@en ; + cif-:_type.contents "Real"@en ; + cif-:_type.purpose "Number"@en ; + cif-:_type.source "Recorded"@en ; + cif-:_units.code "kilopascals"@en ; + rdfs:subClassOf :DIFFRN, + cif-:Number, + cif-:Real, + cif-:Recorded, + cif-:Single . + +:_diffrn.ambient_pressure_su a owl:Class ; + :prefLabel "_diffrn.ambient_pressure_su"@en ; + cif-:_alias.definition_id "['_diffrn_ambient_pressure_su', '_diffrn.ambient_pressure_esd']"@en ; + cif-:_definition.id "_diffrn.ambient_pressure_su"@en ; + cif-:_definition.update "2021-03-03"@en ; + cif-:_description.text """ + Standard uncertainty of the mean hydrostatic pressure + at which intensities were measured."""@en ; + cif-:_name.category_id "diffrn"@en ; + cif-:_name.linked_item_id "_diffrn.ambient_pressure"@en ; + cif-:_name.object_id "ambient_pressure_su"@en ; + cif-:_type.container "Single"@en ; + cif-:_type.contents "Real"@en ; + cif-:_type.purpose "SU"@en ; + cif-:_type.source "Recorded"@en ; + cif-:_units.code "kilopascals"@en ; + rdfs:subClassOf :DIFFRN, + cif-:Real, + cif-:Recorded, + cif-:SU, + cif-:Single . + +:prefLabel a owl:AnnotationProperty ; + :prefLabel "prefLabel"@en ; + rdfs:subPropertyOf rdfs:label . + +:CIF_CORE a owl:Class ; + :prefLabel "CIF_CORE"@en ; + cif-:_definition.class "Head"@en ; + cif-:_definition.id "CIF_CORE"@en ; + cif-:_definition.scope "Category"@en ; + cif-:_definition.update "2014-06-18"@en ; + cif-:_description.text """ + The CIF_CORE group contains the definitions of data items that + are common to all domains of crystallographic studies."""@en ; + cif-:_name.category_id "CORE_DIC"@en ; + cif-:_name.object_id "CIF_CORE"@en ; + rdfs:subClassOf :CORE_DIC . + +:CORE_DIC a owl:Class ; + :prefLabel "CORE_DIC"@en ; + cif-:_description.text """ + The CIF_CORE dictionary records all the CORE data items defined + and used with in the Crystallographic Information Framework (CIF)."""@en ; + cif-:_dictionary.class "Instance"@en ; + cif-:_dictionary.date "2021-08-18"@en ; + cif-:_dictionary.ddl_conformance "4.0.1"@en ; + cif-:_dictionary.namespace "CifCore"@en ; + cif-:_dictionary.title "CORE_DIC"@en ; + cif-:_dictionary.uri "https://raw.githubusercontent.com/COMCIFS/cif_core/master/cif_core.dic"@en ; + cif-:_dictionary.version "3.1.0"@en ; + rdfs:subClassOf cif-:DictionaryDefinedItem . + +:DIFFRACTION a owl:Class ; + :prefLabel "DIFFRACTION"@en ; + cif-:_definition.class "Set"@en ; + cif-:_definition.id "DIFFRACTION"@en ; + cif-:_definition.scope "Category"@en ; + cif-:_definition.update "2012-11-26"@en ; + cif-:_description.text """ + The DICTIONARY group encompassing the CORE DIFFRACTION data items defined + and used with in the Crystallographic Information Framework (CIF)."""@en ; + cif-:_name.category_id "CIF_CORE"@en ; + cif-:_name.object_id "DIFFRACTION"@en ; + rdfs:subClassOf :CIF_CORE . + +:DIFFRN a owl:Class ; + :prefLabel "DIFFRN"@en ; + cif-:_definition.class "Set"@en ; + cif-:_definition.id "DIFFRN"@en ; + cif-:_definition.scope "Category"@en ; + cif-:_definition.update "2012-12-13"@en ; + cif-:_description.text """ + The CATEGORY of data items used to describe the diffraction experiment."""@en ; + cif-:_name.category_id "DIFFRACTION"@en ; + cif-:_name.object_id "DIFFRN"@en ; + rdfs:subClassOf :DIFFRACTION . + diff --git a/tests/dic2owl/test_cli.py b/tests/dic2owl/test_cli.py new file mode 100644 index 0000000..bd76705 --- /dev/null +++ b/tests/dic2owl/test_cli.py @@ -0,0 +1,102 @@ +"""Tests for `dic2owl.cli`.""" +# pylint: disable=import-outside-toplevel +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from typing import Callable + + from .conftest import CLIRunner + + +@pytest.mark.parametrize("use_subprocess", [True, False]) +def test_version(clirunner: "CLIRunner", use_subprocess: bool) -> None: + """Test `--version`.""" + from dic2owl import __version__ + + output = clirunner(["--version"], use_subprocess=use_subprocess) + assert output.stdout == f"dic2owl version {__version__}\n" + + +@pytest.mark.parametrize("use_subprocess", [True, False]) +def test_local_file( + clirunner: "CLIRunner", + top_dir: Path, + cif_ttl: str, + create_location_free_ttl: "Callable[[Path], str]", + use_subprocess: bool, +) -> None: + """Test a normal/default run with minimum input. + + NOTE: The comment conerning the file location has been removed from the + static test Turtle file. + """ + from tempfile import TemporaryDirectory + + with TemporaryDirectory() as tmpdir: + options = [ + str(top_dir / "tests/dic2owl/static/cif_core_minimized.dic") + ] + output = clirunner( + options, run_dir=tmpdir, use_subprocess=use_subprocess + ) + + assert ( + "downloading" in output.stdout + ), f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" + + generated_ttl = create_location_free_ttl( + Path(tmpdir) / "cif_core_minimized.ttl" + ) + + assert generated_ttl == cif_ttl + + content = [_ for _ in Path(tmpdir).iterdir() if _.is_file()] + assert len(content) == 4, ( + "Since `dic2owl` downloads 3 files and the TTL file is generated " + "here, the temporary folder should contain a total of 4 files, " + f"but instead it contains {len(content)} file(s): {content}" + ) + + +@pytest.mark.parametrize("use_subprocess", [True, False]) +def test_output( + clirunner: "CLIRunner", + top_dir: Path, + cif_ttl: str, + create_location_free_ttl: "Callable[[Path], str]", + use_subprocess: bool, +) -> None: + """Test `--output`. + + NOTE: The comment conerning the file location has been removed from the + static test Turtle file. + """ + from tempfile import TemporaryDirectory + + with TemporaryDirectory() as tmpdir: + out_ttl = f"{tmpdir}/test.ttl" + options = [ + "--output", + out_ttl, + str(top_dir / "tests/dic2owl/static/cif_core_minimized.dic"), + ] + output = clirunner(options, use_subprocess=use_subprocess) + + assert ( + "downloading" in output.stdout + ), f"STDOUT: {output.stdout}\nSTDERR: {output.stderr}" + + generated_ttl = create_location_free_ttl(Path(out_ttl)) + + assert generated_ttl == cif_ttl + + content = [_ for _ in Path(tmpdir).iterdir() if _.is_file()] + assert len(content) == 1, ( + "Since `dic2owl` downloads 3 files, but in another directory and " + "only the generated TTL file is in the temporary folder, there " + "should only be 1 file in the temporary folder. But instead it " + f"contains {len(content)} file(s): {content}" + ) 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