From 634b2848656f9b1404172aef0b73a817290c05eb Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Mon, 9 May 2022 19:19:04 +0200 Subject: [PATCH 1/3] TST: Redirect test stdout to the qmflows logger --- conftest.py | 23 +++++++++++++++++++++++ src/qmflows/test_utils.py | 39 +++++++++++++++++++++++++++++++++++++++ test/test_adf_mock.py | 3 ++- test/test_cp2k_mm_mock.py | 9 --------- test/test_sphinx.py | 3 +++ test/test_utils.py | 3 ++- 6 files changed, 69 insertions(+), 11 deletions(-) diff --git a/conftest.py b/conftest.py index c4721c91..f8ae171d 100644 --- a/conftest.py +++ b/conftest.py @@ -1,12 +1,18 @@ """A pytest ``conftest.py`` file.""" +import os import sys import types +import tempfile import importlib +import contextlib from typing import Generator from pathlib import Path import pytest +from scm.plams import config + +from qmflows import InitRestart _ROOT = Path("src") / "qmflows" _collect_ignore = [ @@ -54,3 +60,20 @@ def reload_qmflows() -> Generator[None, None, None]: _del_all_attr(module) importlib.import_module("qmflows") + + +@pytest.fixture(autouse=True, scope="session") +def configure_plams_logger() -> "Generator[None, None, None]": + """Remove the date/time prefix from the PLAMS logging output.""" + # Ensure the plams.config dict is populated by firing up plams.init once + with open(os.devnull, "w") as f1, tempfile.TemporaryDirectory() as f2: + with contextlib.redirect_stdout(f1), InitRestart(f2): + pass + + assert "log" in config + log_backup = config.log.copy() + config.log.time = False + config.log.date = False + + yield None + config.log = log_backup diff --git a/src/qmflows/test_utils.py b/src/qmflows/test_utils.py index be7c9600..3e89d8b7 100755 --- a/src/qmflows/test_utils.py +++ b/src/qmflows/test_utils.py @@ -32,16 +32,20 @@ .. autodata:: requires_ams .. autodata:: requires_adf .. autofunction:: find_executable +.. autodata:: stdout_to_logger """ import os import sys import textwrap +import logging +import contextlib from pathlib import Path import pytest +from .logger import logger from ._settings import Settings from .warnings_qmflows import Assertion_Warning from .packages import Result @@ -58,6 +62,7 @@ 'requires_ams', 'requires_adf', 'find_executable', + 'stdout_to_logger', ] try: @@ -261,3 +266,37 @@ def _has_env_vars(*env_vars: str) -> bool: not _has_env_vars("ADFBIN", "ADFHOME", "ADFRESOURCES"), reason="Requires ADF <=2019", ) + + +class _StdOutToLogger(contextlib.redirect_stdout, contextlib.ContextDecorator): + """A context decorator and file-like object for redirecting the stdout stream to a logger. + + Attributes + ---------- + logger : logging.Logger + The wrapped logger. + level : int + The logging level. + + """ + + __slots__ = ("logger", "level") + + def __init__(self, logger: logging.Logger, level: int = logging.INFO) -> None: + super().__init__(self) + self.logger = logger + self.level = level + + def write(self, msg: str) -> None: + """Log '`msg'` with the integer severity :attr:`level`.""" + self.logger.log(self.level, msg) + + def flush(self) -> None: + """Ensure aqll logging output has been flushed.""" + for handler in self.logger.handlers: + handler.flush() + + +#: A context decorator and file-like object for redirecting the stdout +#: stream to the qmflows logger. +stdout_to_logger = _StdOutToLogger(logger) diff --git a/test/test_adf_mock.py b/test/test_adf_mock.py index c9a0e4aa..d860d75a 100644 --- a/test/test_adf_mock.py +++ b/test/test_adf_mock.py @@ -10,7 +10,7 @@ from qmflows import adf, templates from qmflows.packages import ADF_Result -from qmflows.test_utils import PATH, PATH_MOLECULES +from qmflows.test_utils import PATH, PATH_MOLECULES, stdout_to_logger from qmflows.warnings_qmflows import QMFlows_Warning from qmflows.utils import InitRestart @@ -51,6 +51,7 @@ def test_adf_mock(mocker: MockFixture): ("bob", "Generic property 'bob' not defined", None), ("energy", "It is not possible to retrieve property: 'energy'", "crashed"), ], ids=["undefined_property", "job_crashed"]) +@stdout_to_logger def test_getattr_warning(tmp_path: Path, name: str, match: str, status: Optional[str]) -> None: mol = Molecule(PATH_MOLECULES / "acetonitrile.xyz") jobname = "ADFjob" diff --git a/test/test_cp2k_mm_mock.py b/test/test_cp2k_mm_mock.py index a3fc897c..75881ed9 100644 --- a/test/test_cp2k_mm_mock.py +++ b/test/test_cp2k_mm_mock.py @@ -1,7 +1,5 @@ """Mock CP2K funcionality.""" -import os -import shutil from typing import Callable import numpy as np @@ -10,7 +8,6 @@ from scm.plams import Molecule from qmflows import Settings, cp2k_mm, singlepoint, geometry, freq, md, cell_opt -from qmflows.utils import InitRestart from qmflows.packages import CP2KMM_Result from qmflows.test_utils import get_mm_settings, validate_status, PATH, PATH_MOLECULES @@ -20,12 +17,6 @@ #: Example input Settings for CP2K mm calculations. SETTINGS: Settings = get_mm_settings() -# Ensure that plams.config is populated with a JobManager -with InitRestart(PATH, 'tmp'): - pass -if os.path.isdir(PATH / 'tmp'): - shutil.rmtree(PATH / 'tmp') - def overlap_coords(xyz1: np.ndarray, xyz2: np.ndarray) -> np.ndarray: """Rotate *xyz1* such that it overlaps with *xyz2* using the Kabsch algorithm.""" diff --git a/test/test_sphinx.py b/test/test_sphinx.py index 74e386c0..d9be7dcc 100644 --- a/test/test_sphinx.py +++ b/test/test_sphinx.py @@ -4,6 +4,7 @@ from pathlib import Path import pytest +from qmflows.test_utils import stdout_to_logger try: from sphinx.application import Sphinx @@ -28,6 +29,7 @@ def test_sphinx_build(tmp_path: Path) -> None: tmp_path / "build" / "doctrees", buildername='html', warningiserror=True, + status=stdout_to_logger, ) app.build(force_all=True) except SphinxWarning as ex: @@ -38,3 +40,4 @@ def test_sphinx_build(tmp_path: Path) -> None: warning = RuntimeWarning(str(ex)) warning.__cause__ = ex warnings.warn(warning) + pytest.xfail(str(ex)) diff --git a/test/test_utils.py b/test/test_utils.py index 1f7fae99..b60a31fc 100755 --- a/test/test_utils.py +++ b/test/test_utils.py @@ -12,7 +12,7 @@ from qmflows import Settings from qmflows.packages import CP2K_Result from qmflows.utils import to_runtime_error, file_to_context, init_restart, InitRestart -from qmflows.test_utils import PATH_MOLECULES, PATH, validate_status +from qmflows.test_utils import PATH_MOLECULES, PATH, validate_status, stdout_to_logger PSF_STR: str = """ PSF EXT @@ -63,6 +63,7 @@ def test_file_to_context() -> None: assertion.assert_(file_to_context, 5.0, exception=TypeError) +@stdout_to_logger def test_restart_init(tmp_path: Path) -> None: """Tests for :func:`restart_init` and :class:`RestartInit`.""" workdir = tmp_path / 'test_restart_init' From 2ffe697cf5c8a4c0c0535afd375aef45e7197859 Mon Sep 17 00:00:00 2001 From: Bas van Beek <43369155+BvB93@users.noreply.github.com> Date: Mon, 9 May 2022 20:32:50 +0200 Subject: [PATCH 2/3] TST: Re-enable the sphinx tests --- docs/_packages.rst | 10 +++++----- docs/conf.py | 20 ++++---------------- docs/settings.rst | 4 ++-- src/qmflows/_settings.py | 2 +- src/qmflows/packages/__init__.py | 7 +++++++ src/qmflows/packages/_cp2k_mm.py | 2 +- src/qmflows/packages/_package_wrapper.py | 10 +++++----- src/qmflows/packages/_scm.py | 2 +- test/test_sphinx.py | 1 - 9 files changed, 26 insertions(+), 32 deletions(-) diff --git a/docs/_packages.rst b/docs/_packages.rst index ab6e1938..99f88d76 100644 --- a/docs/_packages.rst +++ b/docs/_packages.rst @@ -117,8 +117,8 @@ API | -.. autofunction:: qmflows.packages.adf -.. autofunction:: qmflows.packages.dftb -.. autofunction:: qmflows.packages.cp2k -.. autofunction:: qmflows.packages.cp2k_mm -.. autofunction:: qmflows.packages.orca +.. autofunction:: qmflows.adf +.. autofunction:: qmflows.dftb +.. autofunction:: qmflows.cp2k +.. autofunction:: qmflows.cp2k_mm +.. autofunction:: qmflows.orca diff --git a/docs/conf.py b/docs/conf.py index c1a0ef26..47910ae8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -21,6 +21,7 @@ # import sys # sys.path.insert(0, os.path.abspath('.')) +import qmflows # -- General configuration ------------------------------------------------ @@ -68,16 +69,16 @@ # built documents. # # The short X.Y version. -version = '0.12' +version = qmflows.__version__ # The full version, including alpha/beta/rc tags. -release = '0.12.0' +release = qmflows.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -204,16 +205,3 @@ # 'none' – Do not show typehints # New in version 2.1. autodoc_typehints = 'none' - -# This value contains a list of modules to be mocked up. -# This is useful when some external dependencies are not met at build time and break the building process. -# You may only specify the root package of the dependencies themselves and omit the sub-modules: -autodoc_mock_imports = [ - 'rdkit', - 'h5py', -] - -rst_epilog = """ -.. |Package| replace:: :class:`~qmflows.packages.Package` -.. |Settings| replace:: :class:`~qmflows.Settings` -""" diff --git a/docs/settings.rst b/docs/settings.rst index 9c1f4b67..629c2b62 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -1,7 +1,7 @@ Settings -------- -|Settings| is a :class:`dict` subclass implemented in PLAMS_ and modified in *Qmflows*. +:class:`~qmflows.Settings` is a :class:`dict` subclass implemented in PLAMS_ and modified in *Qmflows*. This class represents the data in a hierarchical tree-like structure. for example: .. code:: python @@ -19,7 +19,7 @@ This class represents the data in a hierarchical tree-like structure. for exampl >>> input_settings = templates.singlepoint.overlay(s) # (4) -The above code snippet shows how to create a |Settings| instance object in **(1)**, +The above code snippet shows how to create a :class:`~qmflows.Settings` instance object in **(1)**, then in **(2)** the generic keyword *basis* declares that the "DZP" should be used together with the *large* keyword of *ADF* as shown at **(3)**. Finally in line **(4)** the user's keywords are merged with the defaults resultin in a input like: diff --git a/src/qmflows/_settings.py b/src/qmflows/_settings.py index 52475581..0801f054 100644 --- a/src/qmflows/_settings.py +++ b/src/qmflows/_settings.py @@ -60,7 +60,7 @@ def __deepcopy__(self: _Self, _: object) -> _Self: return self.copy() def overlay(self: _Self, other: Mapping[str, Any]) -> _Self: - """Return new instance of |Settings| that is a copy of this instance updated with *other*.""" # noqa: E501 + """Return new instance of :class:`~qmflows.Settings` that is a copy of this instance updated with *other*.""" # noqa: E501 ret = self.copy() ret.update(other) return ret diff --git a/src/qmflows/packages/__init__.py b/src/qmflows/packages/__init__.py index 64920ac3..821fa0ee 100644 --- a/src/qmflows/packages/__init__.py +++ b/src/qmflows/packages/__init__.py @@ -1,5 +1,7 @@ """A set of modules for managing various quantum-chemical packages.""" +from typing import TYPE_CHECKING + from ._packages import ( Package, Result, run, _load_properties as load_properties, @@ -24,3 +26,8 @@ 'ORCA_Result', 'ORCA', 'orca', 'PackageWrapper', 'ResultWrapper', 'JOB_MAP', ] + +if not TYPE_CHECKING: + #: Placeholder docstring for sphinx. + JOB_MAP: "dict[type[plams.Job], Package]" +del TYPE_CHECKING diff --git a/src/qmflows/packages/_cp2k_mm.py b/src/qmflows/packages/_cp2k_mm.py index fe98fb4d..3ac0717a 100644 --- a/src/qmflows/packages/_cp2k_mm.py +++ b/src/qmflows/packages/_cp2k_mm.py @@ -30,7 +30,7 @@ class CP2KMM(CP2K): It uses plams together with the templates to generate the stucture input and also uses Plams to invoke the binary CP2K code. This class is not intended to be called directly by the user, instead the - :func:`~qmflows.packages.cp2k_mm` function should be called. + :class:`~qmflows.cp2k_mm` function should be called. """ # noqa: E501 diff --git a/src/qmflows/packages/_package_wrapper.py b/src/qmflows/packages/_package_wrapper.py index 3f75e73f..82272ace 100644 --- a/src/qmflows/packages/_package_wrapper.py +++ b/src/qmflows/packages/_package_wrapper.py @@ -7,9 +7,9 @@ appropiate instance of Package subclas instance is called. For example, passing :class:`plams.ADFJob` will -automatically call :data:`~qmflows.packages.adf`, +automatically call :data:`~qmflows.adf`, :class:`plams.Cp2kJob` will -call :data:`~qmflows.packages.cp2k`, *etc*. +call :data:`~qmflows.cp2k`, *etc*. When no appropiate Package is found, let's say after passing the :class:`MyFancyJob` type, the PackageWrapper class will still run the job as usual and return the matching @@ -77,7 +77,7 @@ :annotation: : dict[type[plams.Job], Package] A dictionary mapping PLAMS :class:`Job` types - to appropiate QMFlows :class:`~qmflows.packages.Package` instance + to appropiate QMFlows :class:`~qmflows.packages.Package` instance. .. code:: python @@ -119,7 +119,7 @@ plams.Job = plams.core.basejob.Job plams.ORCAJob = plams.interfaces.thirdparty.orca.ORCAJob -__all__ = ['PackageWrapper', 'ResultWrapper'] +__all__ = ['PackageWrapper', 'ResultWrapper', 'JOB_MAP'] JT = TypeVar("JT", bound=plams.core.basejob.Job) @@ -177,7 +177,7 @@ class PackageWrapper(Package, Generic[JT]): See Also -------- - :data:`~qmflows.packages.package_wrapper.JOB_MAP` : :class:`dict[type[plams.Job], Package] ` + :data:`~qmflows.packages.JOB_MAP` A dictionary mapping PLAMS Job types to appropiate QMFlows Package instances. """ # noqa diff --git a/src/qmflows/packages/_scm.py b/src/qmflows/packages/_scm.py index cbad762b..44d8c697 100644 --- a/src/qmflows/packages/_scm.py +++ b/src/qmflows/packages/_scm.py @@ -236,7 +236,7 @@ def run_job(cls, settings: Settings, mol: plams.Molecule, """Execute ADF job. :param settings: user input settings. - :type settings: |Settings| + :type settings: :class:`~qmflows.Settings` :param mol: Molecule to run the simulation :type mol: Plams Molecule :parameter input_file_name: The user can provide a name for the diff --git a/test/test_sphinx.py b/test/test_sphinx.py index d9be7dcc..9ebf77d1 100644 --- a/test/test_sphinx.py +++ b/test/test_sphinx.py @@ -18,7 +18,6 @@ @pytest.mark.skipif(not HAS_SPHINX, reason='Requires Sphinx') -@pytest.mark.xfail def test_sphinx_build(tmp_path: Path) -> None: """Test :meth:`sphinx.application.Sphinx.build`.""" try: From 13086e79788b6e2cb758676ff2271c0ef02a4599 Mon Sep 17 00:00:00 2001 From: Bas van Beek Date: Tue, 10 May 2022 00:41:26 +0200 Subject: [PATCH 3/3] MAINT: Ensure that the qmflows logger actually prints to the stdout stream --- conftest.py | 12 +++++++++++- src/qmflows/__init__.py | 2 +- src/qmflows/_logger.py | 18 ++++++++++++++++++ src/qmflows/logger.py | 8 -------- src/qmflows/test_utils.py | 3 +-- 5 files changed, 31 insertions(+), 12 deletions(-) create mode 100644 src/qmflows/_logger.py delete mode 100644 src/qmflows/logger.py diff --git a/conftest.py b/conftest.py index f8ae171d..8564a360 100644 --- a/conftest.py +++ b/conftest.py @@ -12,7 +12,8 @@ import pytest from scm.plams import config -from qmflows import InitRestart +from qmflows import InitRestart, logger +from qmflows._logger import stdout_handler _ROOT = Path("src") / "qmflows" _collect_ignore = [ @@ -77,3 +78,12 @@ def configure_plams_logger() -> "Generator[None, None, None]": yield None config.log = log_backup + + +@pytest.fixture(autouse=True, scope="session") +def prepare_logger() -> "Generator[None, None, None]": + """Remove logging output to the stdout stream while running tests.""" + assert stdout_handler in logger.handlers + logger.removeHandler(stdout_handler) + yield None + logger.addHandler(stdout_handler) diff --git a/src/qmflows/__init__.py b/src/qmflows/__init__.py index 3acbb4b7..f001ab4e 100644 --- a/src/qmflows/__init__.py +++ b/src/qmflows/__init__.py @@ -6,7 +6,7 @@ from ._version import __version__ as __version__ from ._version_info import version_info as version_info -from .logger import logger +from ._logger import logger from .utils import InitRestart diff --git a/src/qmflows/_logger.py b/src/qmflows/_logger.py new file mode 100644 index 00000000..28c5ffc7 --- /dev/null +++ b/src/qmflows/_logger.py @@ -0,0 +1,18 @@ +"""A module containing the :class:`~logging.Logger` of QMFlows.""" + +import sys +import logging + +__all__ = ['logger', 'stdout_handlet'] + +#: The QMFlows :class:`~logging.Logger`. +logger = logging.getLogger(__package__) +logger.setLevel(logging.DEBUG) + +stdout_handler = logging.StreamHandler(stream=sys.stdout) +stdout_handler.setLevel(logging.DEBUG) +stdout_handler.setFormatter(logging.Formatter( + fmt='[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%H:%M:%S', +)) +logger.addHandler(stdout_handler) diff --git a/src/qmflows/logger.py b/src/qmflows/logger.py deleted file mode 100644 index 8a129d8c..00000000 --- a/src/qmflows/logger.py +++ /dev/null @@ -1,8 +0,0 @@ -"""A module containing the :class:`~logging.Logger` of QMFlows.""" - -import logging - -__all__ = ['logger'] - -#: The QMFlows :class:`~logging.Logger`. -logger = logging.getLogger(__package__) diff --git a/src/qmflows/test_utils.py b/src/qmflows/test_utils.py index 3e89d8b7..347cca03 100755 --- a/src/qmflows/test_utils.py +++ b/src/qmflows/test_utils.py @@ -45,8 +45,7 @@ import pytest -from .logger import logger -from ._settings import Settings +from . import logger, Settings from .warnings_qmflows import Assertion_Warning from .packages import Result