diff --git a/src/picologging/__init__.py b/src/picologging/__init__.py index 14c0fa6..1c1bfc3 100644 --- a/src/picologging/__init__.py +++ b/src/picologging/__init__.py @@ -1,4 +1,6 @@ +import atexit import io +import itertools import os import sys import warnings @@ -518,3 +520,42 @@ def makeLogRecord(dict): for k, v in dict.items(): setattr(rv, k, v) return rv + + +def shutdown(handlerList=None): + """ + Perform any cleanup actions in the logging system (e.g. flushing + buffers). + Should be called at application exit. + """ + handlers = [logger.handlers for logger in root.manager.loggerDict.values()] + [ + root.handlers + ] + handlerList = list(itertools.chain(*handlers)) + + for h in reversed(handlerList): + # errors might occur, for example, if files are locked + # we just ignore them if raiseExceptions is not set + try: + if h: + try: + h.acquire() + # MemoryHandlers might not want to be flushed on close, + # but circular imports prevent us scoping this to just + # those handlers. hence the default to True. + if getattr(h, "flushOnClose", True): + h.flush() + h.close() + except (OSError, ValueError): + # Ignore errors which might be caused + # because handlers have been closed but + # references to them are still around at + # application exit. + pass + finally: + h.release() + except: # ignore everything, as we're shutting down + raise + + +atexit.register(shutdown) diff --git a/src/picologging/__init__.pyi b/src/picologging/__init__.pyi index 7350c9d..ecffb1a 100644 --- a/src/picologging/__init__.pyi +++ b/src/picologging/__init__.pyi @@ -4,7 +4,17 @@ from io import TextIOWrapper from multiprocessing import Manager from string import Template from types import TracebackType -from typing import Any, Generic, Optional, Pattern, TextIO, TypeVar, Union, overload +from typing import ( + Any, + Generic, + Optional, + Pattern, + TextIO, + TypeVar, + Union, + overload, + Sequence, +) from _typeshed import StrPath, SupportsWrite from typing_extensions import Literal, TypeAlias @@ -373,3 +383,6 @@ BASIC_FORMAT: str def getLevelName(level: _Level) -> Any: ... def makeLogRecord(dict: Mapping[str, object]) -> LogRecord: ... +def shutdown( + handlerList: Sequence[Any] = ..., +) -> None: ... # handlerList is undocumented diff --git a/tests/unit/test_picologging.py b/tests/unit/test_picologging.py index a8d3b93..fcc0956 100644 --- a/tests/unit/test_picologging.py +++ b/tests/unit/test_picologging.py @@ -3,6 +3,7 @@ import pytest import picologging +from picologging.handlers import BufferingHandler levels = [ (picologging.DEBUG, "DEBUG"), @@ -150,3 +151,15 @@ def test_make_log_record(): @pytest.mark.parametrize("encoding", ["utf-8", None]) def test_basic_config_encoding(encoding): picologging.basicConfig(filename="test.txt", encoding=encoding) + + +def test_shutdown(): + handler = BufferingHandler(capacity=1) + logger = picologging.getLogger("test") + logger.setLevel(picologging.DEBUG) + logger.addHandler(handler) + logger.debug("test") + + picologging.shutdown() + + assert handler.buffer == []