From 745866669845f6f88c881253725343a0b461a743 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 6 Apr 2021 00:46:07 +0100 Subject: [PATCH] contrib.logging: cleanup & docs --- .meta/.readme.rst | 27 +++ README.rst | 58 ++----- tests/contrib/__init__.py | 0 ...ts_logging.py => tests_contrib_logging.py} | 23 ++- tqdm/contrib/logging.py | 164 +++++------------- 5 files changed, 102 insertions(+), 170 deletions(-) delete mode 100644 tests/contrib/__init__.py rename tests/{contrib/tests_logging.py => tests_contrib_logging.py} (90%) diff --git a/.meta/.readme.rst b/.meta/.readme.rst index 97bf461fc..d299164b9 100644 --- a/.meta/.readme.rst +++ b/.meta/.readme.rst @@ -1102,6 +1102,33 @@ A reusable canonical example is given below: # After the `with`, printing is restored print("Done!") +Redirecting ``logging`` +~~~~~~~~~~~~~~~~~~~~~~~ + +Similar to ``sys.stdout``/``sys.stderr`` as detailed above, console ``logging`` +may also be redirected to ``tqdm.write()``. + +Warning: if also redirecting ``sys.stdout``/``sys.stderr``, make sure to +redirect ``logging`` first if needed. + +Helper methods are available in ``tqdm.contrib.logging``. For example: + +.. code:: python + + import logging + from tqdm import trange + from tqdm.contrib.logging import logging_redirect_tqdm + + LOG = logging.getLogger(__name__) + + if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + with logging_redirect_tqdm(): + for i in trange(9): + if i == 4: + LOG.info("console logging redirected to `tqdm.write()`") + # logging restored + Monitoring thread, intervals and miniters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.rst b/README.rst index 0ea0e98b0..c906e60cf 100644 --- a/README.rst +++ b/README.rst @@ -1321,58 +1321,32 @@ A reusable canonical example is given below: # After the `with`, printing is restored print("Done!") -Redirecting console logging to tqdm -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Redirecting ``logging`` +~~~~~~~~~~~~~~~~~~~~~~~ -Similar to redirecting ``sys.stdout`` directly as detailed in the previous section, -you may want to redirect logging that would otherwise go to the -console (``sys.stdout`` or ``sys.stderr``) to ``tqdm``. +Similar to ``sys.stdout``/``sys.stderr`` as detailed above, console ``logging`` +may also be redirected to ``tqdm.write()``. -Note: if you are also replace ``sys.stdout`` and ``sys.stderr`` at the same time, -then the logging should be redirected first. Otherwise it won't be able to detect -the console logging handler. +Warning: if also redirecting ``sys.stdout``/``sys.stderr``, make sure to +redirect ``logging`` first if needed. -For that you may use ``redirect_logging_to_tqdm`` or ``tqdm_with_logging_redirect`` -from ``tqdm.contrib.logging``. Both methods accept the following optional parameters: - -- ``loggers``: A list of loggers to update. Defaults to ``logging.root``. -- ``tqdm``: A ``tqdm`` class. Defaults to ``tqdm.tqdm``. - -An example redirecting the console logging to tqdm: +Helper methods are available in ``tqdm.contrib.logging``. For example: .. code:: python import logging - from tqdm.contrib.logging import redirect_logging_to_tqdm - - LOGGER = logging.getLogger(__name__) - - if __name__ == '__main__': - logging.basicConfig(level='INFO') - with redirect_logging_to_tqdm(): - # logging to the console is now redirected to tqdm - LOGGER.info('some message') - # logging is now restored - -An similar example, wrapping tqdm while redirecting console logging: - -.. code:: python - - import logging - from tqdm.contrib.logging import tqdm_with_logging_redirect + from tqdm import trange + from tqdm.contrib.logging import logging_redirect_tqdm - LOGGER = logging.getLogger(__name__) + LOG = logging.getLogger(__name__) if __name__ == '__main__': - logging.basicConfig(level='INFO') - - file_list = ['file1', 'file2'] - with tqdm_with_logging_redirect(total=len(file_list)) as pbar: - # logging to the console is now redirected to tqdm - for filename in file_list: - LOGGER.info('processing file: %s', filename) - pbar.update(1) - # logging is now restored + logging.basicConfig(level=logging.INFO) + with logging_redirect_tqdm(): + for i in trange(9): + if i == 4: + LOG.info("console logging redirected to `tqdm.write()`") + # logging restored Monitoring thread, intervals and miniters ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/tests/contrib/__init__.py b/tests/contrib/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/contrib/tests_logging.py b/tests/tests_contrib_logging.py similarity index 90% rename from tests/contrib/tests_logging.py rename to tests/tests_contrib_logging.py index 0b3342257..e2affa786 100644 --- a/tests/contrib/tests_logging.py +++ b/tests/tests_contrib_logging.py @@ -1,6 +1,5 @@ # pylint: disable=missing-module-docstring, missing-class-docstring # pylint: disable=missing-function-docstring, no-self-use - from __future__ import absolute_import import logging @@ -13,9 +12,9 @@ from tqdm import tqdm from tqdm.contrib.logging import _get_first_found_console_logging_formatter from tqdm.contrib.logging import _TqdmLoggingHandler as TqdmLoggingHandler -from tqdm.contrib.logging import redirect_logging_to_tqdm, tqdm_with_logging_redirect +from tqdm.contrib.logging import logging_redirect_tqdm, tqdm_logging_redirect -from ..tests_tqdm import importorskip +from .tests_tqdm import importorskip LOGGER = logging.getLogger(__name__) @@ -101,7 +100,7 @@ def test_should_return_stream_handler_formatter_if_stream_is_stderr(self): class TestRedirectLoggingToTqdm: def test_should_add_and_remove_tqdm_handler(self): logger = logging.Logger('test') - with redirect_logging_to_tqdm(loggers=[logger]): + with logging_redirect_tqdm(loggers=[logger]): assert len(logger.handlers) == 1 assert isinstance(logger.handlers[0], TqdmLoggingHandler) assert not logger.handlers @@ -111,7 +110,7 @@ def test_should_remove_and_restore_console_handlers(self): stderr_console_handler = logging.StreamHandler(sys.stderr) stdout_console_handler = logging.StreamHandler(sys.stderr) logger.handlers = [stderr_console_handler, stdout_console_handler] - with redirect_logging_to_tqdm(loggers=[logger]): + with logging_redirect_tqdm(loggers=[logger]): assert len(logger.handlers) == 1 assert isinstance(logger.handlers[0], TqdmLoggingHandler) assert logger.handlers == [stderr_console_handler, stdout_console_handler] @@ -122,14 +121,14 @@ def test_should_inherit_console_logger_formatter(self): console_handler = logging.StreamHandler(sys.stderr) console_handler.setFormatter(formatter) logger.handlers = [console_handler] - with redirect_logging_to_tqdm(loggers=[logger]): + with logging_redirect_tqdm(loggers=[logger]): assert logger.handlers[0].formatter == formatter def test_should_not_remove_stream_handlers_not_fot_stdout_or_stderr(self): logger = logging.Logger('test') stream_handler = logging.StreamHandler(StringIO()) logger.addHandler(stream_handler) - with redirect_logging_to_tqdm(loggers=[logger]): + with logging_redirect_tqdm(loggers=[logger]): assert len(logger.handlers) == 2 assert logger.handlers[0] == stream_handler assert isinstance(logger.handlers[1], TqdmLoggingHandler) @@ -139,7 +138,7 @@ def test_should_not_remove_stream_handlers_not_fot_stdout_or_stderr(self): class TestTqdmWithLoggingRedirect: def test_should_add_and_remove_handler_from_root_logger_by_default(self): original_handlers = list(logging.root.handlers) - with tqdm_with_logging_redirect(total=1) as pbar: + with tqdm_logging_redirect(total=1) as pbar: assert isinstance(logging.root.handlers[-1], TqdmLoggingHandler) LOGGER.info('test') pbar.update(1) @@ -147,7 +146,7 @@ def test_should_add_and_remove_handler_from_root_logger_by_default(self): def test_should_add_and_remove_handler_from_custom_logger(self): logger = logging.Logger('test') - with tqdm_with_logging_redirect(total=1, loggers=[logger]) as pbar: + with tqdm_logging_redirect(total=1, loggers=[logger]) as pbar: assert len(logger.handlers) == 1 assert isinstance(logger.handlers[0], TqdmLoggingHandler) logger.info('test') @@ -157,7 +156,7 @@ def test_should_add_and_remove_handler_from_custom_logger(self): def test_should_not_fail_with_logger_without_console_handler(self): logger = logging.Logger('test') logger.handlers = [] - with tqdm_with_logging_redirect(total=1, loggers=[logger]): + with tqdm_logging_redirect(total=1, loggers=[logger]): logger.info('test') assert not logger.handlers @@ -169,14 +168,14 @@ def test_should_format_message(self): )) logger.handlers = [console_handler] CustomTqdm.messages = [] - with tqdm_with_logging_redirect(loggers=[logger], tqdm=CustomTqdm): + with tqdm_logging_redirect(loggers=[logger], tqdm_class=CustomTqdm): logger.info('test') assert CustomTqdm.messages == ['prefix:test'] def test_use_root_logger_by_default_and_write_to_custom_tqdm(self): logger = logging.root CustomTqdm.messages = [] - with tqdm_with_logging_redirect(total=1, tqdm=CustomTqdm) as pbar: + with tqdm_logging_redirect(total=1, tqdm_class=CustomTqdm) as pbar: assert isinstance(pbar, CustomTqdm) logger.info('test') assert CustomTqdm.messages == ['test'] diff --git a/tqdm/contrib/logging.py b/tqdm/contrib/logging.py index 08c9c3c40..5f70944dc 100644 --- a/tqdm/contrib/logging.py +++ b/tqdm/contrib/logging.py @@ -1,7 +1,5 @@ - """ -Enables multiple commonly used features relating to logging -in combination with tqdm. +Helper functionality for interoperability with stdlib `logging`. """ from __future__ import absolute_import @@ -12,28 +10,23 @@ try: from typing import Iterator, List, Optional, Type # pylint: disable=unused-import except ImportError: - # we may ignore type hints pass -from ..std import tqdm as _tqdm +from ..std import tqdm as std_tqdm class _TqdmLoggingHandler(logging.StreamHandler): def __init__( self, - tqdm=None # type: Optional[Type[tqdm.tqdm]] + tqdm_class=std_tqdm # type: Type[std_tqdm] ): - super( # pylint: disable=super-with-arguments - _TqdmLoggingHandler, self - ).__init__() - if tqdm is None: - tqdm = _tqdm - self.tqdm = tqdm + super(_TqdmLoggingHandler, self).__init__() + self.tqdm_class = tqdm_class def emit(self, record): try: msg = self.format(record) - self.tqdm.write(msg) + self.tqdm_class.write(msg) self.flush() except (KeyboardInterrupt, SystemExit): raise @@ -42,88 +35,69 @@ def emit(self, record): def _is_console_logging_handler(handler): - return ( - isinstance(handler, logging.StreamHandler) - and handler.stream in {sys.stdout, sys.stderr} - ) + return (isinstance(handler, logging.StreamHandler) + and handler.stream in {sys.stdout, sys.stderr}) def _get_first_found_console_logging_formatter(handlers): for handler in handlers: if _is_console_logging_handler(handler): return handler.formatter - return None @contextmanager -def redirect_logging_to_tqdm( +def logging_redirect_tqdm( loggers=None, # type: Optional[List[logging.Logger]], - tqdm=None # type: Optional[Type[tqdm.tqdm]] + tqdm_class=std_tqdm # type: Type[std_tqdm] ): # type: (...) -> Iterator[None] """ - Context manager for redirecting logging console output to tqdm. - Logging to other logging handlers, such as a log file, - will not be affected. - - By default the, the handlers of the root logger will be amended. - (for the duration of the context) - You may also provide a list of `loggers` instead - (e.g. if a particular logger doesn't fallback to the root logger) + Context manager redirecting console logging to `tqdm.write()`, leaving + other logging handlers (e.g. log files) unaffected. - Example: + Parameters + ---------- + loggers : list, optional + Which handlers to redirect (default: [logging.root]). + tqdm_class : optional + Example + ------- ```python import logging - from tqdm.contrib.logging import redirect_logging_to_tqdm + from tqdm import trange + from tqdm.contrib.logging import logging_redirect_tqdm - LOGGER = logging.getLogger(__name__) + LOG = logging.getLogger(__name__) if __name__ == '__main__': - logging.basicConfig(level='INFO') - with redirect_logging_to_tqdm(): - # logging to the console is now redirected to tqdm - LOGGER.info('some message') - # logging is now restored + logging.basicConfig(level=logging.INFO) + with logging_redirect_tqdm(): + for i in trange(9): + if i == 4: + LOG.info("console logging redirected to `tqdm.write()`") + # logging restored ``` """ if loggers is None: loggers = [logging.root] - original_handlers_list = [ - logger.handlers for logger in loggers - ] + original_handlers_list = [logger.handlers for logger in loggers] try: for logger in loggers: - tqdm_handler = _TqdmLoggingHandler(tqdm) + tqdm_handler = _TqdmLoggingHandler(tqdm_class) tqdm_handler.setFormatter( - _get_first_found_console_logging_formatter( - logger.handlers - ) - ) + _get_first_found_console_logging_formatter(logger.handlers)) logger.handlers = [ - handler - for handler in logger.handlers - if not _is_console_logging_handler(handler) - ] + [tqdm_handler] + handler for handler in logger.handlers + if not _is_console_logging_handler(handler)] + [tqdm_handler] yield finally: for logger, original_handlers in zip(loggers, original_handlers_list): logger.handlers = original_handlers -def _pop_optional( - kwargs, # type: dict - key, # type: str - default_value=None -): - try: - return kwargs.pop(key) - except KeyError: - return default_value - - @contextmanager -def tqdm_with_logging_redirect( +def tqdm_logging_redirect( *args, # loggers=None, # type: Optional[List[logging.Logger]] # tqdm=None, # type: Optional[Type[tqdm.tqdm]] @@ -131,64 +105,22 @@ def tqdm_with_logging_redirect( ): # type: (...) -> Iterator[None] """ - Similar to `redirect_logging_to_tqdm`, - but provides a context manager wrapping tqdm. - - All parameters, except `loggers` and `tqdm`, will get passed on to `tqdm`. - - By default this will wrap `tqdm.tqdm`. - You may pass your own `tqdm` class if desired. - - Example: - + Convenience shortcut for: ```python - import logging - from tqdm.contrib.logging import tqdm_with_logging_redirect - - LOGGER = logging.getLogger(__name__) - - if __name__ == '__main__': - logging.basicConfig(level='INFO') - - file_list = ['file1', 'file2'] - with tqdm_with_logging_redirect(total=len(file_list)) as pbar: - # logging to the console is now redirected to tqdm - for filename in file_list: - LOGGER.info('processing file: %s', filename) - pbar.update(1) - # logging is now restored - ``` - - A more advanced example with non-default tqdm class and loggers: - - ```python - import logging - from tqdm.auto import tqdm - from tqdm.contrib.logging import tqdm_with_logging_redirect - - LOGGER = logging.getLogger(__name__) - - if __name__ == '__main__': - logging.basicConfig(level='INFO') - - file_list = ['file1', 'file2'] - with tqdm_with_logging_redirect( - total=len(file_list), - tqdm=tqdm, - loggers=[LOGGER] - ) as pbar: - # logging to the console is now redirected to tqdm - for filename in file_list: - LOGGER.info('processing file: %s', filename) - pbar.update(1) - # logging is now restored + with tqdm_class(*args, **tqdm_kwargs) as pbar: + with logging_redirect_tqdm(loggers=loggers, tqdm_class=tqdm_class): + yield pbar ``` + Parameters + ---------- + tqdm_class : optional, (default: tqdm.std.tqdm). + loggers : optional, list. + **tqdm_kwargs : passed to `tqdm_class`. """ - loggers = _pop_optional(kwargs, 'loggers') - tqdm = _pop_optional(kwargs, 'tqdm') - if tqdm is None: - tqdm = _tqdm - with tqdm(*args, **kwargs) as pbar: - with redirect_logging_to_tqdm(loggers=loggers, tqdm=tqdm): + tqdm_kwargs = kwargs.copy() + loggers = tqdm_kwargs.pop('loggers', None) + tqdm_class = tqdm_kwargs.pop('tqdm_class', std_tqdm) + with tqdm_class(*args, **tqdm_kwargs) as pbar: + with logging_redirect_tqdm(loggers=loggers, tqdm_class=tqdm_class): yield pbar