Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TST: Redirect test stdout to the qmflows logger and re-enable sphinx tests #297

Merged
merged 3 commits into from
May 10, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
"""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, logger
from qmflows._logger import stdout_handler

_ROOT = Path("src") / "qmflows"
_collect_ignore = [
@@ -54,3 +61,29 @@ 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


@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)
10 changes: 5 additions & 5 deletions docs/_packages.rst
Original file line number Diff line number Diff line change
@@ -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
20 changes: 4 additions & 16 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -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`
"""
4 changes: 2 additions & 2 deletions docs/settings.rst
Original file line number Diff line number Diff line change
@@ -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:
2 changes: 1 addition & 1 deletion src/qmflows/__init__.py
Original file line number Diff line number Diff line change
@@ -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

18 changes: 18 additions & 0 deletions src/qmflows/_logger.py
Original file line number Diff line number Diff line change
@@ -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)
2 changes: 1 addition & 1 deletion src/qmflows/_settings.py
Original file line number Diff line number Diff line change
@@ -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
8 changes: 0 additions & 8 deletions src/qmflows/logger.py

This file was deleted.

7 changes: 7 additions & 0 deletions src/qmflows/packages/__init__.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion src/qmflows/packages/_cp2k_mm.py
Original file line number Diff line number Diff line change
@@ -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

10 changes: 5 additions & 5 deletions src/qmflows/packages/_package_wrapper.py
Original file line number Diff line number Diff line change
@@ -7,9 +7,9 @@
appropiate instance of Package subclas instance is called.

For example, passing :class:`plams.ADFJob<scm.plams.interfaces.adfsuite.adf.ADFJob>` will
automatically call :data:`~qmflows.packages.adf`,
automatically call :data:`~qmflows.adf`,
:class:`plams.Cp2kJob<scm.plams.interfaces.thirdparty.cp2k.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<scm.plams.core.basejob.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] <dict>`
:data:`~qmflows.packages.JOB_MAP`
A dictionary mapping PLAMS Job types to appropiate QMFlows Package instances.

""" # noqa
2 changes: 1 addition & 1 deletion src/qmflows/packages/_scm.py
Original file line number Diff line number Diff line change
@@ -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
40 changes: 39 additions & 1 deletion src/qmflows/test_utils.py
Original file line number Diff line number Diff line change
@@ -32,17 +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 ._settings import Settings
from . import logger, Settings
from .warnings_qmflows import Assertion_Warning
from .packages import Result

@@ -58,6 +61,7 @@
'requires_ams',
'requires_adf',
'find_executable',
'stdout_to_logger',
]

try:
@@ -261,3 +265,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)
3 changes: 2 additions & 1 deletion test/test_adf_mock.py
Original file line number Diff line number Diff line change
@@ -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"
9 changes: 0 additions & 9 deletions test/test_cp2k_mm_mock.py
Original file line number Diff line number Diff line change
@@ -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."""
4 changes: 3 additions & 1 deletion test/test_sphinx.py
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@
from pathlib import Path

import pytest
from qmflows.test_utils import stdout_to_logger

try:
from sphinx.application import Sphinx
@@ -17,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:
@@ -28,6 +28,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 +39,4 @@ def test_sphinx_build(tmp_path: Path) -> None:
warning = RuntimeWarning(str(ex))
warning.__cause__ = ex
warnings.warn(warning)
pytest.xfail(str(ex))
3 changes: 2 additions & 1 deletion test/test_utils.py
Original file line number Diff line number Diff line change
@@ -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'