diff --git a/mhctools/__init__.py b/mhctools/__init__.py index eeddc20..6a54314 100644 --- a/mhctools/__init__.py +++ b/mhctools/__init__.py @@ -1,5 +1,6 @@ from .binding_prediction import BindingPrediction from .binding_prediction_collection import BindingPredictionCollection +from .logging import set_log_level from .pred import ( FIELD_BEST_DIRECTIONS, Kind, @@ -87,7 +88,7 @@ def __getattr__(name): raise AttributeError( "module %r has no attribute %r" % (__name__, name)) -__version__ = "3.31.3" +__version__ = "3.31.4" __all__ = [ "Prediction", @@ -108,6 +109,7 @@ def __getattr__(name): "parse_annotation_spec", "BindingPrediction", "BindingPredictionCollection", + "set_log_level", "IedbNetMHCcons", "IedbNetMHCpan", "IedbSMM", diff --git a/mhctools/logging.py b/mhctools/logging.py index a8f10a6..72894aa 100644 --- a/mhctools/logging.py +++ b/mhctools/logging.py @@ -11,12 +11,46 @@ # limitations under the License. import logging -import logging.config -from importlib.resources import as_file, files + + +_PACKAGE_LOGGER_NAME = "mhctools" +_PACKAGE_LOGGER = logging.getLogger(_PACKAGE_LOGGER_NAME) +if not any(isinstance(handler, logging.NullHandler) + for handler in _PACKAGE_LOGGER.handlers): + _PACKAGE_LOGGER.addHandler(logging.NullHandler()) + +_LOG_LEVELS = { + "CRITICAL": logging.CRITICAL, + "ERROR": logging.ERROR, + "WARNING": logging.WARNING, + "WARN": logging.WARNING, + "INFO": logging.INFO, + "DEBUG": logging.DEBUG, + "NOTSET": logging.NOTSET, +} def get_logger(name): - config_resource = files("mhctools").joinpath("logging.conf") - with as_file(config_resource) as config_path: - logging.config.fileConfig(str(config_path)) return logging.getLogger(name) + + +def set_log_level(level): + """Set the log level for mhctools loggers. + + Parameters + ---------- + level : str or int + Logging level such as ``"WARNING"``, ``"INFO"``, or ``logging.DEBUG``. + + Returns + ------- + logging.Logger + The package logger whose level was set. + """ + if isinstance(level, str): + level_name = level.upper() + if level_name not in _LOG_LEVELS: + raise ValueError("Unknown logging level %r" % level) + level = _LOG_LEVELS[level_name] + _PACKAGE_LOGGER.setLevel(level) + return _PACKAGE_LOGGER diff --git a/tests/test_logging.py b/tests/test_logging.py new file mode 100644 index 0000000..dba55d6 --- /dev/null +++ b/tests/test_logging.py @@ -0,0 +1,76 @@ +import logging +import logging.config + +import pytest + +import mhctools +from mhctools import logging as mhctools_logging + + +@pytest.fixture(autouse=True) +def restore_mhctools_logger(): + logger = logging.getLogger("mhctools") + old_level = logger.level + old_handlers = list(logger.handlers) + old_propagate = logger.propagate + yield + logger.setLevel(old_level) + logger.handlers[:] = old_handlers + logger.propagate = old_propagate + + +def test_get_logger_does_not_call_fileconfig(monkeypatch): + def fail_fileconfig(*args, **kwargs): + raise AssertionError("fileConfig must not be called by mhctools") + + monkeypatch.setattr(logging.config, "fileConfig", fail_fileconfig) + + logger = mhctools_logging.get_logger("mhctools.test") + + assert logger.name == "mhctools.test" + + +def test_get_logger_does_not_reconfigure_root_logger(): + root = logging.getLogger() + old_level = root.level + old_handlers = list(root.handlers) + + mhctools_logging.get_logger("mhctools.test") + + assert root.level == old_level + assert list(root.handlers) == old_handlers + + +def test_package_logger_has_null_handler_by_default(): + handlers = logging.getLogger("mhctools").handlers + + assert any(isinstance(handler, logging.NullHandler) + for handler in handlers) + + +def test_info_logs_are_quiet_by_default(capsys): + logger = mhctools_logging.get_logger("mhctools.test.quiet") + + logger.info("should not print") + + captured = capsys.readouterr() + assert captured.out == "" + assert captured.err == "" + + +def test_set_log_level_accepts_string_levels(): + logger = mhctools.set_log_level("WARNING") + + assert logger is logging.getLogger("mhctools") + assert logger.level == logging.WARNING + + +def test_set_log_level_accepts_numeric_levels(): + logger = mhctools_logging.set_log_level(logging.ERROR) + + assert logger.level == logging.ERROR + + +def test_set_log_level_rejects_unknown_level(): + with pytest.raises(ValueError, match="NOTALEVEL"): + mhctools.set_log_level("NOTALEVEL")