diff --git a/package/MDAnalysis/__init__.py b/package/MDAnalysis/__init__.py index 6843c3738a..fe9023f694 100644 --- a/package/MDAnalysis/__init__.py +++ b/package/MDAnalysis/__init__.py @@ -200,7 +200,7 @@ from .lib import log from .lib.log import start_logging, stop_logging -logging.getLogger("MDAnalysis").addHandler(log.NullHandler()) +logging.getLogger(__name__).addHandler(logging.NullHandler()) del logging # only MDAnalysis DeprecationWarnings are loud by default diff --git a/package/MDAnalysis/lib/log.py b/package/MDAnalysis/lib/log.py index d63ec54782..ab6e2c4128 100644 --- a/package/MDAnalysis/lib/log.py +++ b/package/MDAnalysis/lib/log.py @@ -86,33 +86,75 @@ """ import sys import logging -import re +import os from tqdm.auto import tqdm +# from MDAnalysis.lib.util import deprecate + from .. import version +# Things to deprecated: +# logfile -> stream (bc it could be any stream like object) +# +# version? Why would any user want to modify version in the first place? +# Just have the message logged it directly + -def start_logging(logfile="MDAnalysis.log", version=version.__version__): +def start_logging(stream="MDAnalysis.log", version=version.__version__): """Start logging of messages to file and console. The default logfile is named `MDAnalysis.log` and messages are logged with the tag *MDAnalysis*. """ - create("MDAnalysis", logfile=logfile) - logging.getLogger("MDAnalysis").info( - "MDAnalysis %s STARTED logging to %r", version, logfile + create( + "MDAnalysis", + stream=stream, + level="DEBUG", + fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", + ) + create( + "MDAnalysis", + stream=sys.stdout, + level="INFO", + fmt="%(name)-12s: %(levelname)-8s %(message)s", + ) + logging.getLogger(__name__).info( + f"MDAnalysis {version} STARTED logging to {stream!r}" ) def stop_logging(): """Stop logging to logfile and console.""" - logger = logging.getLogger("MDAnalysis") + logger = logging.getLogger(__name__) logger.info("MDAnalysis STOPPED logging") clear_handlers(logger) # this _should_ do the job... -def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"): +# Things to deprecated overall: +# log.NullHandler -> replace with logging.NullHandler() warning +# create() -> add_handler() (potentially confusing bc standard library +# has the same method but in camelcase weirdly?) +# or create_handler()? +# +# For create(): +# logfile -> stream (bc it could be any stream like object, not just logfiles) +# logger_name should be deprecated. +# +# Standard library recommends constructing through +# logging.getLogger(__name__) because all loggers share the same namespace +# and this is a systemmatic way of defining loggers +# +# __name__ is MDAnalysis +# See 2nd paragraph: https://docs.python.org/3/library/logging.html#logger-objects +# +def create( + logger_name="MDAnalysis", + stream="MDAnalysis.log", + level="DEBUG", + fmt=None, + mode="a", +): """Create a top level logger. - The file logger logs everything (including DEBUG). @@ -130,25 +172,23 @@ def create(logger_name="MDAnalysis", logfile="MDAnalysis.log"): http://docs.python.org/library/logging.html?#logging-to-multiple-destinations """ + # replaced with logging.getLogger(__name__) logger = logging.getLogger(logger_name) - logger.setLevel(logging.DEBUG) - - # handler that writes to logfile - logfile_handler = logging.FileHandler(logfile) - logfile_formatter = logging.Formatter( - "%(asctime)s %(name)-12s %(levelname)-8s %(message)s" - ) - logfile_handler.setFormatter(logfile_formatter) - logger.addHandler(logfile_handler) - - # define a Handler which writes INFO messages or higher to the sys.stderr - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.INFO) - # set a format which is simpler for console use - formatter = logging.Formatter("%(name)-12s: %(levelname)-8s %(message)s") - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) + # This checks for file-like object per duck typing + # https://docs.python.org/3/library/logging.handlers.html#streamhandler + if hasattr(stream, "write") and hasattr(stream, "flush"): + handler = logging.StreamHandler(stream) + elif isinstance(stream, (str, os.PathLike)): + handler = logging.FileHandler(stream, mode=mode) + else: + raise TypeError( + "Input Stream is neither a string, PathLike object or a stream" + ) + + handler.setLevel(level.upper()) + handler.setFormatter(logging.Formatter(fmt)) + logger.addHandler(handler) return logger @@ -159,27 +199,10 @@ def clear_handlers(logger): (only important for reload/debug cycles...) """ - for h in logger.handlers: + for h in list(logger.handlers): logger.removeHandler(h) -class NullHandler(logging.Handler): - """Silent Handler. - - Useful as a default:: - - h = NullHandler() - logging.getLogger("MDAnalysis").addHandler(h) - del h - - see the advice on logging and libraries in - http://docs.python.org/library/logging.html?#configuring-logging-for-a-library - """ - - def emit(self, record): - pass - - class ProgressBar(tqdm): r"""Display a visual progress bar and time estimate. diff --git a/testsuite/MDAnalysisTests/lib/test_log.py b/testsuite/MDAnalysisTests/lib/test_log.py index 541660ca4c..d8a0a6e9e2 100644 --- a/testsuite/MDAnalysisTests/lib/test_log.py +++ b/testsuite/MDAnalysisTests/lib/test_log.py @@ -20,12 +20,43 @@ # MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. # J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 # -import warnings -import pytest +import sys +import logging +import MDAnalysis as mda + +from os.path import basename from MDAnalysis.lib.log import ProgressBar +class TestConvenienceFunctions: + def test_start_logging(self, tmp_path): + mda.start_logging(tmp_path / "MDAnalysis.log") + logger = logging.getLogger("MDAnalysis") + + # Test expected handlers' presence and behavior + assert any(isinstance(h, logging.NullHandler) for h in logger.handlers) + any( + isinstance(h, logging.FileHandler) + and basename(h.stream.name) == "MDAnalysis.log" + and h.level == logging.DEBUG + for h in logger.handlers + ) + assert any( + isinstance(h, logging.StreamHandler) + and h.stream is sys.stdout + and h.level == logging.INFO + for h in logger.handlers + ) + + def test_stop_logging(self, tmp_path): + mda.lib.log.start_logging(tmp_path / "MDAnalysis.log") + logger = logging.getLogger("MDAnalysis") + mda.lib.log.stop_logging() + + assert len(logger.handlers) == 0 + + class TestProgressBar(object): def test_output(self, capsys): diff --git a/testsuite/MDAnalysisTests/utils/test_log.py b/testsuite/MDAnalysisTests/utils/test_log.py deleted file mode 100644 index 0abaed2795..0000000000 --- a/testsuite/MDAnalysisTests/utils/test_log.py +++ /dev/null @@ -1,75 +0,0 @@ -# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- -# vim: tabstop=4 expandtab shiftwidth=4 softtabstop=4 fileencoding=utf-8 -# -# MDAnalysis --- https://www.mdanalysis.org -# Copyright (c) 2006-2017 The MDAnalysis Development Team and contributors -# (see the file AUTHORS for the full list of names) -# -# Released under the Lesser GNU Public Licence, v2.1 or any higher version -# -# Please cite your use of MDAnalysis in published work: -# -# R. J. Gowers, M. Linke, J. Barnoud, T. J. E. Reddy, M. N. Melo, S. L. Seyler, -# D. L. Dotson, J. Domanski, S. Buchoux, I. M. Kenney, and O. Beckstein. -# MDAnalysis: A Python package for the rapid analysis of molecular dynamics -# simulations. In S. Benthall and S. Rostrup editors, Proceedings of the 15th -# Python in Science Conference, pages 102-109, Austin, TX, 2016. SciPy. -# doi: 10.25080/majora-629e541a-00e -# -# N. Michaud-Agrawal, E. J. Denning, T. B. Woolf, and O. Beckstein. -# MDAnalysis: A Toolkit for the Analysis of Molecular Dynamics Simulations. -# J. Comput. Chem. 32 (2011), 2319--2327, doi:10.1002/jcc.21787 -# -from io import StringIO - -import logging -import sys - -import MDAnalysis -import MDAnalysis.lib.log -import pytest - - -def test_start_stop_logging(): - try: - MDAnalysis.log.start_logging() - logger = logging.getLogger("MDAnalysis") - logger.info("Using the MDAnalysis logger works") - except Exception as err: - raise AssertionError("Problem with logger: {0}".format(err)) - finally: - MDAnalysis.log.stop_logging() - - -class RedirectedStderr(object): - """Temporarily replaces sys.stderr with *stream*. - - Deals with cached stderr, see - http://stackoverflow.com/questions/6796492/temporarily-redirect-stdout-stderr - """ - - def __init__(self, stream=None): - self._stderr = sys.stderr - self.stream = stream or sys.stdout - - def __enter__(self): - self.old_stderr = sys.stderr - self.old_stderr.flush() - sys.stderr = self.stream - - def __exit__(self, exc_type, exc_value, traceback): - self._stderr.flush() - sys.stderr = self.old_stderr - - -@pytest.fixture() -def buffer(): - return StringIO() - - -def _assert_in(output, string): - assert ( - string in output - ), "Output '{0}' does not match required format '{1}'.".format( - output.replace("\r", "\\r"), string.replace("\r", "\\r") - )