Skip to content

Commit

Permalink
Merge pull request #297 from SCM-NV/logger
Browse files Browse the repository at this point in the history
TST: Redirect test stdout to the qmflows logger and re-enable sphinx tests
  • Loading branch information
BvB93 authored May 10, 2022
2 parents 13e9f0f + 13086e7 commit b9c2795
Show file tree
Hide file tree
Showing 17 changed files with 124 additions and 53 deletions.
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 = [
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Up @@ -21,6 +21,7 @@
# import sys
# sys.path.insert(0, os.path.abspath('.'))

import qmflows

# -- General configuration ------------------------------------------------

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion src/qmflows/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
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
Expand Up @@ -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
Expand Down
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,
Expand All @@ -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
Expand Up @@ -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

Expand Down
10 changes: 5 additions & 5 deletions src/qmflows/packages/_package_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/qmflows/packages/_scm.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
40 changes: 39 additions & 1 deletion src/qmflows/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

try:
Expand Down Expand Up @@ -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
Expand Up @@ -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

Expand Down Expand Up @@ -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"
Expand Down
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
Expand All @@ -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

Expand All @@ -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."""
Expand Down
4 changes: 3 additions & 1 deletion test/test_sphinx.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path

import pytest
from qmflows.test_utils import stdout_to_logger

try:
from sphinx.application import Sphinx
Expand All @@ -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:
Expand All @@ -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:
Expand All @@ -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))
Loading

0 comments on commit b9c2795

Please sign in to comment.