From 2a29728f6d41fb4a399b2d55c6a7a8695bfbf45e Mon Sep 17 00:00:00 2001 From: Philip Loche Date: Tue, 9 Jul 2024 13:49:12 +0200 Subject: [PATCH] Cleanup logger function --- RMSD_rmsd.csv | 103 ++++++++++++++++++++++++ data/.traj.trr_offsets.lock | 0 docs/CHANGELOG.rst | 1 + src/mdacli/logger.py | 94 +++++++++++++++++----- tests/test_logger.py | 155 +++++++++++++++++++++++++----------- tox.ini | 9 ++- 6 files changed, 290 insertions(+), 72 deletions(-) create mode 100644 RMSD_rmsd.csv create mode 100644 data/.traj.trr_offsets.lock diff --git a/RMSD_rmsd.csv b/RMSD_rmsd.csv new file mode 100644 index 0000000..d234dbd --- /dev/null +++ b/RMSD_rmsd.csv @@ -0,0 +1,103 @@ +# Command line was: mda RMSD -f data/traj.trr -s data/topol.tpr -atomgroup all -v +# +0.0 0.0 2.75841341423869e-07 +1.0 0.4000000059604645 4.81326115892731 +2.0 0.800000011920929 5.357346963405206 +3.0 1.2000000476837158 6.390734837796418 +4.0 1.600000023841858 6.5917707099361955 +5.0 2.0 7.120691623117086 +6.0 2.4000000953674316 7.0932971703456404 +7.0 2.799999952316284 7.210072442189706 +8.0 3.200000047683716 7.350055263419901 +9.0 3.5999999046325684 7.844873843255679 +10.0 4.0 8.180087119212413 +11.0 4.400000095367432 8.227688239882058 +12.0 4.800000190734863 8.614257002857027 +13.0 5.199999809265137 8.62122036557813 +14.0 5.599999904632568 8.67343663635793 +15.0 6.0 8.908314639001917 +16.0 6.400000095367432 9.22277720732986 +17.0 6.800000190734863 9.45809196703627 +18.0 7.199999809265137 9.494556715642583 +19.0 7.599999904632568 9.851845566465697 +20.0 8.0 9.468063262801405 +21.0 8.399999618530273 9.72237181955554 +22.0 8.800000190734863 9.795817807721628 +23.0 9.199999809265137 10.205362972924204 +24.0 9.600000381469727 10.27637925301259 +25.0 10.0 10.242532811673563 +26.0 10.399999618530273 10.475580487935385 +27.0 10.800000190734863 10.76729154066856 +28.0 11.199999809265137 10.638146968254457 +29.0 11.600000381469727 10.637528008136737 +30.0 12.0 10.945558487284588 +31.0 12.399999618530273 11.119148252661983 +32.0 12.800000190734863 11.036844299482748 +33.0 13.199999809265137 11.149991488308283 +34.0 13.600000381469727 11.490146370071116 +35.0 14.0 11.293276708054744 +36.0 14.399999618530273 11.465510093595096 +37.0 14.800000190734863 11.361783839087757 +38.0 15.199999809265137 11.688307451417892 +39.0 15.600000381469727 11.677958185946274 +40.0 16.0 11.725397471965591 +41.0 16.399999618530273 11.792187633973917 +42.0 16.799999237060547 12.079675543212032 +43.0 17.200000762939453 12.017340816368135 +44.0 17.600000381469727 11.832623095416574 +45.0 18.0 12.078582430542404 +46.0 18.399999618530273 12.135870781640552 +47.0 18.799999237060547 12.236794476377922 +48.0 19.200000762939453 12.356653592137196 +49.0 19.600000381469727 12.263444430090598 +50.0 20.0 12.331696858017313 +51.0 20.399999618530273 12.422221381599394 +52.0 20.799999237060547 12.558986484261055 +53.0 21.200000762939453 12.639355584306978 +54.0 21.600000381469727 12.718215456054343 +55.0 22.0 12.607770690425108 +56.0 22.399999618530273 12.697634805758867 +57.0 22.799999237060547 12.758519920522179 +58.0 23.200000762939453 12.705814503914063 +59.0 23.600000381469727 13.017167838684855 +60.0 24.0 12.686333633112097 +61.0 24.399999618530273 12.675852589517227 +62.0 24.799999237060547 12.839709523727292 +63.0 25.200000762939453 12.729883878407353 +64.0 25.600000381469727 12.653439745733921 +65.0 26.0 12.721168883213945 +66.0 26.399999618530273 12.995463681469525 +67.0 26.799999237060547 13.07793381003199 +68.0 27.200000762939453 13.262224704574585 +69.0 27.600000381469727 13.300797004390763 +70.0 28.0 13.238897757255796 +71.0 28.399999618530273 13.087260864183053 +72.0 28.799999237060547 13.099125663939907 +73.0 29.200000762939453 13.256080555506715 +74.0 29.600000381469727 13.1987499792674 +75.0 30.0 13.533923362047654 +76.0 30.399999618530273 13.594879167259311 +77.0 30.799999237060547 13.539118019498249 +78.0 31.200000762939453 13.62617158933157 +79.0 31.600000381469727 13.580435985043335 +80.0 32.0 13.61097076031173 +81.0 32.400001525878906 13.447316292639426 +82.0 32.79999923706055 13.53048613632186 +83.0 33.20000076293945 13.537338845719567 +84.0 33.599998474121094 13.596872138236884 +85.0 34.0 13.657265618233158 +86.0 34.400001525878906 13.844448099158042 +87.0 34.79999923706055 13.82893234282479 +88.0 35.20000076293945 13.70458798342047 +89.0 35.599998474121094 13.84712832747217 +90.0 36.0 13.912169269321316 +91.0 36.400001525878906 13.901216193796094 +92.0 36.79999923706055 13.890424503904844 +93.0 37.20000076293945 13.65238960999978 +94.0 37.599998474121094 13.833306344510708 +95.0 38.0 13.756444793478451 +96.0 38.400001525878906 13.738460985315902 +97.0 38.79999923706055 13.742058599181664 +98.0 39.20000076293945 13.921235214646803 +99.0 39.599998474121094 13.932457595918546 +100.0 40.0 13.879719688365299 diff --git a/data/.traj.trr_offsets.lock b/data/.traj.trr_offsets.lock new file mode 100644 index 0000000..e69de29 diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 63bfe50..a5c4732 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -2,6 +2,7 @@ Changelog ========= +* Cleanup logger function v0.1.31 (2024-07-08) ------------------------------------------ diff --git a/src/mdacli/logger.py b/src/mdacli/logger.py index 7f32cde..80a449a 100644 --- a/src/mdacli/logger.py +++ b/src/mdacli/logger.py @@ -6,18 +6,55 @@ # Released under the GNU Public Licence, v2 or any higher version # SPDX-License-Identifier: GPL-2.0-or-later """Logging.""" - import contextlib import logging import sys +import warnings +from pathlib import Path +from typing import List, Optional, Union from .colors import Emphasise -@contextlib.contextmanager -def setup_logging(logobj, logfile=None, level=logging.WARNING): +def check_suffix(filename: Union[str, Path], suffix: str) -> Union[str, Path]: + """Check the suffix of a file name and adds if it not existing. + + If ``filename`` does not end with ``suffix`` the ``suffix`` is added and a + warning will be issued. + + Parameters + ---------- + filename : Name of the file to be checked. + suffix : Expected filesuffix i.e. ``.txt``. + + Returns + ------- + Checked and probably extended file name. """ - Create a logging environment for a given logobj. + path_filename = Path(filename) + + if path_filename.suffix != suffix: + warnings.warn( + f"The file name should have a '{suffix}' extension. The user " + f"requested the file with name '{filename}', but it will be saved " + f"as '{filename}{suffix}'.", + stacklevel=1, + ) + path_filename = path_filename.parent / (path_filename.name + suffix) + + if type(filename) is str: + return str(path_filename) + else: + return path_filename + + +@contextlib.contextmanager +def setup_logging( + logobj: logging.Logger, + logfile: Optional[Union[str, Path]] = None, + level: int = logging.WARNING, +): + """Create a logging environment for a given ``log_obj``. Parameters ---------- @@ -28,38 +65,51 @@ def setup_logging(logobj, logfile=None, level=logging.WARNING): level : int Set the root logger level to the specified level. If for example set to :py:obj:`logging.DEBUG` detailed debug logs inludcing filename and - function name are displayed. For :py:obj:`logging.INFO only the message - logged from errors, warnings and infos will be displayed. + function name are displayed. For :py:obj:`logging.INFO` only the + message logged from errors, warnings and infos will be displayed. """ try: + format = "" if level == logging.DEBUG: - format = ( - "[{levelname}] {filename}:{name}:{funcName}:{lineno}: " - "{message}" - ) - else: - format = "{message}" + format += "[{levelname}]:{filename}:{funcName}:{lineno} - " + format += "{message}" + + formatter = logging.Formatter(format, style="{") + handlers: List[Union[logging.StreamHandler, logging.FileHandler]] = [] - logging.basicConfig(format=format, - handlers=[logging.StreamHandler(sys.stdout)], - level=level, - style='{') + stream_handler = logging.StreamHandler(sys.stdout) + stream_handler.setFormatter(formatter) + handlers.append(stream_handler) if logfile: - logfile += ".log" * (not logfile.endswith("log")) - handler = logging.FileHandler(filename=logfile, encoding='utf-8') - handler.setFormatter(logging.Formatter(format, style='{')) - logobj.addHandler(handler) + logfile = check_suffix(filename=logfile, suffix=".log") + file_handler = logging.FileHandler( + filename=str(logfile), encoding="utf-8") + file_handler.setFormatter(formatter) + handlers.append(file_handler) else: logging.addLevelName(logging.INFO, Emphasise.info("INFO")) logging.addLevelName(logging.DEBUG, Emphasise.debug("DEBUG")) logging.addLevelName(logging.WARNING, Emphasise.warning("WARNING")) logging.addLevelName(logging.ERROR, Emphasise.error("ERROR")) - logobj.info('Logging to file is disabled.') + + logging.basicConfig( + format=format, handlers=handlers, level=level, style="{") + logging.captureWarnings(True) + + if logfile: + abs_path = str(Path(logfile).absolute().resolve()) + logobj.info(f"This log is also available at {abs_path!r}.") + else: + logobj.debug("Logging to file is disabled.") + + for handler in handlers: + logobj.addHandler(handler) yield + finally: - handlers = logobj.handlers[:] for handler in handlers: + handler.flush() handler.close() logobj.removeHandler(handler) diff --git a/tests/test_logger.py b/tests/test_logger.py index caf1389..6168d1e 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -7,52 +7,111 @@ # SPDX-License-Identifier: GPL-2.0-or-later """Test mdacli logger.""" import logging +import warnings +from pathlib import Path -import mdacli.logger - - -class Test_setup_logger: - """Test the setup_logger.""" - - def test_default_log(self, caplog): - """Default message in STDOUT.""" - caplog.set_level(logging.INFO) - logger = logging.getLogger("test") - with mdacli.logger.setup_logging(logger, - logfile=None, - level=logging.INFO): - logger.info("foo") - assert "foo" in caplog.text - - def test_info_log(self, tmpdir, caplog): - """Default message in STDOUT and file.""" - caplog.set_level(logging.INFO) - logger = logging.getLogger("test") - with tmpdir.as_cwd(): - # Explicityly leave out the .dat file ending to check if this - # is created by the function. - with mdacli.logger.setup_logging(logger, - logfile="logfile", - level=logging.INFO): - logger.info("foo") - assert "foo" in caplog.text - with open("logfile.log", "r") as f: - log = f.read() - - assert "foo" in log - - def test_debug_log(self, tmpdir, caplog): - """Debug message in STDOUT and file.""" - caplog.set_level(logging.INFO) - logger = logging.getLogger("test") - with tmpdir.as_cwd(): - with mdacli.logger.setup_logging(logger, - logfile="logfile", - level=logging.DEBUG): - logger.info("foo") - assert "test:test_logger.py:52 foo\n" in caplog.text - - with open("logfile.log", "r") as f: - log = f.read() - - assert "test_logger.py:test:test_debug_log:52: foo\n" in log +import pytest + +from mdacli.logger import check_suffix, setup_logging + + +@pytest.mark.parametrize("filename", ["example.txt", Path("example.txt")]) +def test_check_suffix(filename): + """Check suffix tetsing.""" + result = check_suffix(filename, ".txt") + + assert str(result) == "example.txt" + assert isinstance(result, type(filename)) + + +@pytest.mark.parametrize("filename", ["example", Path("example")]) +def test_warning_on_missing_suffix(filename): + """Check issued warning in missing suffix.""" + match = r"The file name should have a '\.txt' extension." + with pytest.warns(UserWarning, match=match): + result = check_suffix(filename, ".txt") + + assert str(result) == "example.txt" + assert isinstance(result, type(filename)) + + +def test_warnings_in_log(caplog): + """Test that warnings are forwarded to the logger. + + Keep this test at the top since it seems otherwise there are some pytest + issues... + """ + logger = logging.getLogger() + + with setup_logging(logger): + warnings.warn("A warning", stacklevel=1) + + assert "A warning" in caplog.text + + +def test_default_log(caplog, capsys): + """Default message only in STDOUT.""" + caplog.set_level(logging.INFO) + logger = logging.getLogger() + + with setup_logging(logger, level=logging.INFO): + logger.info("foo") + logger.debug("A debug message") + + stdout_log = capsys.readouterr().out + + assert "Logging to file is disabled." not in caplog.text # DEBUG message + assert "INFO" not in stdout_log + assert "foo" in stdout_log + assert "A debug message" not in stdout_log + + +def test_info_log(caplog, monkeypatch, tmp_path, capsys): + """Default message in STDOUT and file.""" + monkeypatch.chdir(tmp_path) + caplog.set_level(logging.INFO) + logger = logging.getLogger() + + with setup_logging(logger, logfile="logfile.log", level=logging.INFO): + logger.info("foo") + logger.debug("A debug message") + + with open("logfile.log", "r") as f: + file_log = f.read() + + stdout_log = capsys.readouterr().out + log_path = str((tmp_path / "logfile.log").absolute()) + + assert file_log == stdout_log + assert f"This log is also available at '{log_path}'" in caplog.text + + for logtext in [stdout_log, file_log]: + assert "INFO" not in logtext + assert "foo" in logtext + assert "A debug message" not in logtext + + +def test_debug_log(caplog, monkeypatch, tmp_path, capsys): + """Debug message in STDOUT and file.""" + monkeypatch.chdir(tmp_path) + caplog.set_level(logging.DEBUG) + logger = logging.getLogger() + + with setup_logging(logger, logfile="logfile.log", level=logging.DEBUG): + logger.info("foo") + logger.debug("A debug message") + + with open("logfile.log", "r") as f: + file_log = f.read() + + stdout_log = capsys.readouterr().out + log_path = str((tmp_path / "logfile.log").absolute()) + + assert file_log == stdout_log + assert f"This log is also available at '{log_path}'" in caplog.text + + for logtext in [stdout_log, file_log]: + assert "foo" in logtext + assert "A debug message" in logtext + # Test that debug information is in output + assert "test_logger.py:test_debug_log:" in logtext diff --git a/tox.ini b/tox.ini index 01e7e52..27055fd 100644 --- a/tox.ini +++ b/tox.ini @@ -35,7 +35,12 @@ commands_pre = coverage erase # execute pytest commands = - pytest --cov --cov-report=term-missing --cov-append --cov-config=.coveragerc -vv --hypothesis-show-statistics {posargs} + pytest \ + --cov \ + --cov-report=term-missing \ + --cov-append --cov-config=.coveragerc \ + --hypothesis-show-statistics \ + {posargs} # after executing the pytest assembles the coverage reports commands_post = coverage report @@ -53,7 +58,7 @@ deps = skip_install = true commands = flake8 {posargs:src/mdacli tests} - isort --verbose --check-only --diff src/mdacli tests + isort --check-only --diff src/mdacli tests # asserts package build integrity [testenv:build]