diff --git a/docs/CHANGELOG.rst b/docs/CHANGELOG.rst index 247e13a..2e379c1 100644 --- a/docs/CHANGELOG.rst +++ b/docs/CHANGELOG.rst @@ -2,6 +2,8 @@ Changelog ========= +* Replace Boolean ``debug`` option in ``setup_logging`` by more flexible integer + ``level`` parameter. v0.1.29 (2024-03-21) ------------------------------------------ diff --git a/src/mdacli/cli.py b/src/mdacli/cli.py index 5803309..db00d81 100644 --- a/src/mdacli/cli.py +++ b/src/mdacli/cli.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- # -# Copyright (c) 2021 Authors and contributors +# Copyright (c) 2024 Authors and contributors # # Released under the GNU Public Licence, v2 or any higher version # SPDX-License-Identifier: GPL-2.0-or-later @@ -104,13 +104,20 @@ def cli(name, args = ap.parse_args() + # Set the logging level based on the verbose argument + # If verbose is not an argument, default to WARNING + if not hasattr(args, "verbose") or not args.verbose: + level = logging.WARNING + else: + level = logging.INFO + if args.debug: - args.verbose = True + level = logging.DEBUG else: - # Ignore all warnings if not in debug mode + # Ignore all warnings if not in debug mode, because MDA is noisy warnings.filterwarnings("ignore") - with setup_logging(logger, logfile=args.logfile, debug=args.debug): + with setup_logging(logger, logfile=args.logfile, level=level): # Execute the main client interface. try: analysis_callable = args.analysis_callable diff --git a/src/mdacli/logger.py b/src/mdacli/logger.py index b9ae2c2..7f32cde 100644 --- a/src/mdacli/logger.py +++ b/src/mdacli/logger.py @@ -15,7 +15,7 @@ @contextlib.contextmanager -def setup_logging(logobj, logfile=None, debug=False): +def setup_logging(logobj, logfile=None, level=logging.WARNING): """ Create a logging environment for a given logobj. @@ -25,19 +25,20 @@ def setup_logging(logobj, logfile=None, debug=False): A logging instance logfile : str Name of the log file - debug : bool - If ``True`` detailed debug logs inludcing filename and function name - are displayed. If ``False`` only the message logged from - errors, warnings and infos will be displayed. + 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. """ try: - format = '{message}' - if debug: - format = "[{levelname}] {filename}:{name}:{funcName}:{lineno}: " \ - + format - level = logging.DEBUG + if level == logging.DEBUG: + format = ( + "[{levelname}] {filename}:{name}:{funcName}:{lineno}: " + "{message}" + ) else: - level = logging.INFO + format = "{message}" logging.basicConfig(format=format, handlers=[logging.StreamHandler(sys.stdout)], diff --git a/tests/run_tester b/tests/run_tester new file mode 100755 index 0000000..f9daade --- /dev/null +++ b/tests/run_tester @@ -0,0 +1,16 @@ +#!/usr/bin/env python3 +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# +# Copyright (c) 2024 Authors and contributors +# +# Released under the GNU Public Licence, v2 or any higher version +# SPDX-License-Identifier: GPL-2.0-or-later +import re +import sys + +from tester.__main__ import main + + +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/tests/test_cli.py b/tests/test_cli.py index cde45b3..2fa7436 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -8,33 +8,38 @@ """Test mdacli cli.""" import subprocess +import sys +from pathlib import Path import pytest from MDAnalysisTests.datafiles import TPR, XTC +tester_class = (Path('.').absolute() / 'tests/run_tester').as_posix() + + def test_required_args(): """Test that there is a module given.""" with pytest.raises(subprocess.CalledProcessError): - subprocess.check_call(['mda']) + subprocess.check_call(["mda"]) def test_wrong_module(): """Test for a non existent module.""" with pytest.raises(subprocess.CalledProcessError): - subprocess.check_call(['mda', 'foo']) + subprocess.check_call(["mda", "foo"]) -@pytest.mark.parametrize('args', ("version", "debug", "help")) +@pytest.mark.parametrize("args", ("version", "debug", "help")) def test_extra_options(args): """Test for a ab extra option.""" - subprocess.check_call(['mda', '--' + args]) + subprocess.check_call(["mda", "--" + args]) -@pytest.mark.parametrize('args', ("RMSF", "rmsf")) +@pytest.mark.parametrize("args", ("RMSF", "rmsf")) def test_case_insensitive(args): - """Test for beeing case insensitive.""" - subprocess.check_call(['mda', args, "-h"]) + """Test for being case insensitive.""" + subprocess.check_call(["mda", args, "-h"]) @pytest.mark.parametrize('args', ("RMSF", "rmsf")) @@ -48,4 +53,54 @@ def test_running_analysis(tmpdir): """Test running a complete analysis.""" with tmpdir.as_cwd(): subprocess.check_call( - ['mda', "rmsf", "-s", TPR, "-f", XTC, "-atomgroup", "all"]) + ["mda", "rmsf", "-s", TPR, "-f", XTC, "-atomgroup", "all"] + ) + + +def test_verbosity_level_warning(caplog): + """Test the log level warning.""" + # This should only print warning messages + output = subprocess.check_output( + [sys.executable, tester_class, + "tester", "-s", TPR, "-f", XTC, "-atomgroup", "all"], + text=True, + ) + assert "This is a warning" in output + # Cross-check that info and debug messages are not printed + assert "This is a debug message" not in output + assert "This is an info message" not in output + + +def test_verbosity_level_info(caplog): + """Test the log level info.""" + # This should only print warning and info messages + output = subprocess.check_output( + [ + sys.executable, tester_class, + "tester", "-s", TPR, "-f", XTC, + "-atomgroup", "all", + "-v", + ], + text=True, + ) + assert "This is an info message" in output + assert "This is a warning" in output + # Cross-check that debug messages are not printed + assert "This is a debug message" not in output + + +def test_verbosity_level_debug(caplog): + """Test the log level debug.""" + # This should print all messages + output = subprocess.check_output( + [ + sys.executable, tester_class, "--debug", + "tester", "-s", TPR, "-f", XTC, + "-atomgroup", "all", + "-v", + ], + text=True, + ) + assert "This is an info message" in output + assert "This is a warning" in output + assert "This is a debug message" in output diff --git a/tests/test_logger.py b/tests/test_logger.py index b65390d..caf1389 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 # -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- # -# Copyright (c) 2021 Authors and contributors +# Copyright (c) 2024 Authors and contributors # # Released under the GNU Public Licence, v2 or any higher version # SPDX-License-Identifier: GPL-2.0-or-later @@ -20,7 +20,7 @@ def test_default_log(self, caplog): logger = logging.getLogger("test") with mdacli.logger.setup_logging(logger, logfile=None, - debug=False): + level=logging.INFO): logger.info("foo") assert "foo" in caplog.text @@ -33,7 +33,7 @@ def test_info_log(self, tmpdir, caplog): # is created by the function. with mdacli.logger.setup_logging(logger, logfile="logfile", - debug=False): + level=logging.INFO): logger.info("foo") assert "foo" in caplog.text with open("logfile.log", "r") as f: @@ -48,7 +48,7 @@ def test_debug_log(self, tmpdir, caplog): with tmpdir.as_cwd(): with mdacli.logger.setup_logging(logger, logfile="logfile", - debug=True): + level=logging.DEBUG): logger.info("foo") assert "test:test_logger.py:52 foo\n" in caplog.text diff --git a/tests/tester/__init__.py b/tests/tester/__init__.py new file mode 100644 index 0000000..4fd4697 --- /dev/null +++ b/tests/tester/__init__.py @@ -0,0 +1,10 @@ +#!/usr/bin/env python3 +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# +# Copyright (c) 2024 Authors and contributors +# +# Released under the GNU Public Licence, v2 or any higher version +# SPDX-License-Identifier: GPL-2.0-or-later +"""Make the tester module available to mdacli.""" + +from .tester_class import Tester # noqa diff --git a/tests/tester/__main__.py b/tests/tester/__main__.py new file mode 100644 index 0000000..ebf1bab --- /dev/null +++ b/tests/tester/__main__.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# +# Copyright (c) 2024 Authors and contributors +# (see the AUTHORS.rst file for the full list of names) +# +# Released under the GNU Public Licence, v3 or any higher version +# SPDX-License-Identifier: GPL-3.0-or-later +"""Test module for mdacli.""" + +from MDAnalysis.analysis.base import AnalysisBase + +from mdacli import cli + + +def main(): + """Execute main CLI entry point.""" + cli( + name="Tester", + module_list=["tester"], + base_class=AnalysisBase, + version=0.0, + description="test", + ignore_warnings=True, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/tester/tester_class.py b/tests/tester/tester_class.py new file mode 100644 index 0000000..eaa6f35 --- /dev/null +++ b/tests/tester/tester_class.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- Mode: python; tab-width: 4; indent-tabs-mode:nil; coding:utf-8 -*- +# +# Copyright (c) 2024 Authors and contributors +# +# Released under the GNU Public Licence, v2 or any higher version +# SPDX-License-Identifier: GPL-2.0-or-later +"""Mock module for mdacli to test logging.""" + +import logging + +from MDAnalysis.analysis.base import AnalysisBase + + +logger = logging.getLogger(__name__) + + +class Tester(AnalysisBase): + """Mock class for mdacli. Implements only the minimum requirements. + + Currently only logs messages at different levels to check the verbosity and + debug flags in the CLI. + + Parameters + ---------- + atomgroup : AtomGroup or Universe + """ + + def __init__(self, atomgroup, **kwargs): + """Initialise the Tester class.""" + super(Tester, self).__init__(atomgroup.universe.trajectory, **kwargs) + logger.info("This is an info message") + logger.warn("This is a warning") + logger.debug("This is a debug message") + + def _prepare(self): + """Prepare the analysis.""" + pass + + def _single_frame(self): + """Analyse a single frame.""" + pass + + def _conclude(self): + """Conclude the analysis.""" + pass