Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions Lib/logging/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1356,6 +1356,8 @@ def __init__(self, rootnode):
self.disable = 0
self.emittedNoHandlerWarning = False
self.loggerDict = {}
# Loggers whose isEnabledFor cache may be populated; see _clear_cache.
self._cached_loggers = weakref.WeakSet()
self.loggerClass = None
self.logRecordFactory = None

Expand Down Expand Up @@ -1471,9 +1473,11 @@ def _clear_cache(self):
"""

with _lock:
for logger in self.loggerDict.values():
if isinstance(logger, Logger):
logger._cache.clear()
# Only loggers that have populated a cache are visited, so
# setLevel() costs O(loggers with cached results), not O(loggers).
for logger in self._cached_loggers:
logger._cache.clear()
self._cached_loggers.clear()
self.root._cache.clear()

#---------------------------------------------------------------------------
Expand Down Expand Up @@ -1799,6 +1803,7 @@ def isEnabledFor(self, level):
return self._cache[level]
except KeyError:
with _lock:
self.manager._cached_loggers.add(self)
if self.manager.disable >= level:
is_enabled = self._cache[level] = False
else:
Expand Down
32 changes: 32 additions & 0 deletions Lib/test/test_logging.py
Original file line number Diff line number Diff line change
Expand Up @@ -6398,6 +6398,38 @@ def test_pickling(self):
unpickled = pickle.loads(s)
self.assertIs(unpickled, logger)

def test_caching_unregistered_logger(self):
# A logger that is not in loggerDict must not keep a stale cache.
self.addCleanup(logging.root.manager._clear_cache)
logger = logging.Logger("unregistered")
self.assertEqual(logger._cache, {})
logger.setLevel(logging.DEBUG)
self.assertEqual(logger._cache, {})
self.assertTrue(logger.isEnabledFor(logging.DEBUG))
self.assertEqual(logger._cache, {logging.DEBUG: True})
logger.setLevel(logging.ERROR)
self.assertEqual(logger._cache, {})
Comment thread
Ar-maan05 marked this conversation as resolved.
Resolved
self.assertFalse(logger.isEnabledFor(logging.DEBUG))
self.assertEqual(logger._cache, {logging.DEBUG: False})

def test_clear_cache_empties_cached_loggers(self):
manager = logging.root.manager
self.addCleanup(manager._clear_cache)
registered = logging.getLogger("cached.registered")
unregistered = logging.Logger("cached.unregistered")
# Start from a known state: other tests may have cached results.
manager._clear_cache()
Comment on lines +6420 to +6421

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you shoudl also have some cleanup (or if it's not the case, you can add this in setUp() and tearDown()). We don't want the cache to be possibly polluted for the next test.

self.assertEqual(len(manager._cached_loggers), 0)
for logger in (registered, unregistered):
logger.isEnabledFor(logging.DEBUG)
self.assertIn(logger, manager._cached_loggers)
Comment thread
Ar-maan05 marked this conversation as resolved.
Resolved
self.assertEqual(len(manager._cached_loggers), 2)
registered.setLevel(logging.INFO)
self.assertEqual(len(manager._cached_loggers), 0)
# A later cache miss starts tracking the logger again.
registered.isEnabledFor(logging.DEBUG)
self.assertEqual(list(manager._cached_loggers), [registered])

def test_caching(self):
root = self.root_logger
logger1 = logging.getLogger("abc")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Speed up :meth:`logging.Logger.setLevel` and :func:`logging.disable` when
there are many loggers: only loggers that have cached results are now cleared,
instead of every logger. This also clears the cache of loggers that are not
registered with the manager, which could previously keep a stale result.
Loading