From 0dd402fd69d77c3962de1050eb78609da1ed4ea9 Mon Sep 17 00:00:00 2001 From: Weijian He <230282609+Madin-H23@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:59:22 +0800 Subject: [PATCH] Do not override an existing logging configuration. The CSSUTILS logger was created with a StreamHandler and the INFO level every time an ErrorHandler was instantiated, overriding a logging setup the application had already done, and adding a second handler when one was already configured. Install the default handler only when the logger has no handlers at all, and apply the default level only when neither the logger nor the root logger has one, so that a level chosen by the application is preserved. An unconfigured logger keeps the previous behaviour. --- cssutils/errorhandler.py | 19 +++++++--- newsfragments/13.bugfix.rst | 1 + tests/test_errorhandler.py | 70 +++++++++++++++++++++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 newsfragments/13.bugfix.rst diff --git a/cssutils/errorhandler.py b/cssutils/errorhandler.py index f9da5e3d..878462c9 100644 --- a/cssutils/errorhandler.py +++ b/cssutils/errorhandler.py @@ -50,11 +50,20 @@ def __init__(self, log, defaultloglevel=logging.INFO, raiseExceptions=True): import sys self._log = logging.getLogger('CSSUTILS') - hdlr = logging.StreamHandler(sys.stderr) - formatter = logging.Formatter('%(levelname)s\t%(message)s') - hdlr.setFormatter(formatter) - self._log.addHandler(hdlr) - self._log.setLevel(defaultloglevel) + if not self._log.hasHandlers(): + # only install a handler if the application hasn't configured logging + hdlr = logging.StreamHandler(sys.stderr) + formatter = logging.Formatter('%(levelname)s\t%(message)s') + hdlr.setFormatter(formatter) + self._log.addHandler(hdlr) + root = logging.getLogger() + if ( + self._log.level == logging.NOTSET + and not root.handlers + and root.level == logging.WARNING + ): + # only set the default level if the application hasn't chosen one + self._log.setLevel(defaultloglevel) self.raiseExceptions = raiseExceptions diff --git a/newsfragments/13.bugfix.rst b/newsfragments/13.bugfix.rst new file mode 100644 index 00000000..c7992d17 --- /dev/null +++ b/newsfragments/13.bugfix.rst @@ -0,0 +1 @@ +Do not override an existing logging configuration when the CSSUTILS logger is created. diff --git a/tests/test_errorhandler.py b/tests/test_errorhandler.py index 0745b096..4b4ccacd 100644 --- a/tests/test_errorhandler.py +++ b/tests/test_errorhandler.py @@ -8,6 +8,7 @@ import pytest import cssutils +from cssutils.errorhandler import _ErrorHandler @pytest.fixture(autouse=True) @@ -144,3 +145,72 @@ def test_parsevalidation(self): s = self._setHandler() cssutils.parseStyle(style, validate=False) assert s.getvalue() == '' + + +class TestErrorHandlerSetup: + """Tests for the initial configuration of the CSSUTILS logger.""" + + @pytest.fixture + def logger(self, monkeypatch): + "the logger cssutils configures" + logger = logging.getLogger('CSSUTILS') + monkeypatch.setattr(logger, 'level', logging.NOTSET) + root = logging.getLogger() + monkeypatch.setattr(root, 'level', logging.WARNING) + return logger + + def test_installs_default_configuration(self, monkeypatch): + "an unconfigured logger gets the default handler and level" + logger = logging.getLogger('CSSUTILS-TEST') + logger.handlers.clear() + logger.setLevel(logging.NOTSET) + root = logging.getLogger() + monkeypatch.setattr(root, 'handlers', []) + monkeypatch.setattr(root, 'level', logging.WARNING) + real_get_logger = logging.getLogger + monkeypatch.setattr( + logging, + 'getLogger', + lambda name=None: logger if name == 'CSSUTILS' else real_get_logger(name), + ) + + _ErrorHandler(None) + + assert len(logger.handlers) == 1 + assert logger.handlers[0].stream is sys.stderr + assert logger.level == logging.INFO + + def test_keeps_existing_handler(self, logger): + "a handler installed by the application is not replaced" + handler = logging.NullHandler() + logger.addHandler(handler) + before = list(logger.handlers) + + _ErrorHandler(None) + + assert list(logger.handlers) == before + + def test_keeps_configured_level(self, logger): + "a level set for the logger is preserved" + logger.setLevel(logging.ERROR) + + _ErrorHandler(None) + + assert logger.level == logging.ERROR + + def test_keeps_root_level(self, logger): + "a level set for the root logger is preserved" + logging.getLogger().setLevel(logging.ERROR) + + _ErrorHandler(None) + + assert logger.level == logging.NOTSET + + def test_keeps_root_configuration(self, logger, monkeypatch): + "a logging configuration for the root logger is not overridden" + root = logging.getLogger() + monkeypatch.setattr(root, 'handlers', [logging.NullHandler()]) + + _ErrorHandler(None) + + assert logger.level == logging.NOTSET