From 49d4ed3553669ab7d6eea2e3ad31e511b7c3a78d Mon Sep 17 00:00:00 2001 From: Mateusz Bysiek Date: Wed, 5 Nov 2025 17:45:07 +0900 Subject: [PATCH] feat(logging): improve StreamToCall compatibility --- boilerplates/logging.py | 15 ++++++++++++++- test/test_logging.py | 9 ++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/boilerplates/logging.py b/boilerplates/logging.py index 58f0816..b8a9a77 100644 --- a/boilerplates/logging.py +++ b/boilerplates/logging.py @@ -218,12 +218,24 @@ def unittest_verbosity() -> t.Optional[int]: return None -class StreamToCall: +class StreamToCall(t.IO[str]): """Redirect stream writes to a function call. Enable using logging instances as a file-like objects. Given a called_function, convert write(text) calls to called_function(text) calls. For example: StreamToCall(logging.warning) will redirect all writes to logging.warning(). + + Example usage: + + logger = logging.getLogger(__name__) + with contextlib.redirect_stdout(boilerplates.logging.StreamToCall(logger.info)): + print('this will be logged at INFO level') + + Another example: + + logger = logging.getLogger(__name__) + stream = boilerplates.logging.StreamToCall(logger.debug) + print('this will be logged at DEBUG level', file=stream) """ def __init__(self, called_function: collections.abc.Callable): @@ -235,6 +247,7 @@ def write(self, message: str, *args): while message.endswith('\r') or message.endswith('\n'): message = message[:-1] self._function(message, *args) + return len(message) def flush(self): """Flush can be a no-op.""" diff --git a/test/test_logging.py b/test/test_logging.py index dfd5a40..7303089 100644 --- a/test/test_logging.py +++ b/test/test_logging.py @@ -1,5 +1,6 @@ """Unit tests for logging boilerplate.""" +import contextlib import inspect import logging import os @@ -103,7 +104,13 @@ def test_unittest_verbosity_not_unittest(self): verbosity = boilerplates.logging.unittest_verbosity() self.assertIsNone(verbosity) - def test_stream_to_call(self): + def test_stream_to_call_contextlib(self): + log = logging.getLogger(f'{__name__}.test_stream_to_call') + with contextlib.redirect_stdout(boilerplates.logging.StreamToCall(log.info)): + with self.assertLogs(logger=log, level='INFO'): + print('test output') + + def test_stream_to_call_direct(self): log = logging.getLogger(f'{__name__}.test_stream_to_call') stream = boilerplates.logging.StreamToCall(log.info) with self.assertLogs(logger=log, level='INFO'):