From 2fbcdb9f467bb2b43bbfc21b91cf7dcaed181792 Mon Sep 17 00:00:00 2001 From: dombean <46692370+dombean@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:09:06 +0100 Subject: [PATCH 1/4] Added function: init_logger_advanced --- rdsa_utils/helpers/logging.py | 99 ++++++++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 1 deletion(-) diff --git a/rdsa_utils/helpers/logging.py b/rdsa_utils/helpers/logging.py index de9445b..8cc410d 100644 --- a/rdsa_utils/helpers/logging.py +++ b/rdsa_utils/helpers/logging.py @@ -3,7 +3,7 @@ from functools import partial import logging from textwrap import dedent -from typing import Callable, Dict, Optional +from typing import Callable, Dict, List, Optional from humanfriendly import format_timespan import pandas as pd @@ -64,6 +64,103 @@ def init_logger_basic(log_level: int) -> None: """) +def init_logger_advanced( + log_level: int, + handlers: Optional[List[logging.Handler]] = None, + log_format: str = None, + date_format: str = None, +) -> None: + """Instantiate a logger with provided handlers. + + This function allows the logger to be used across modules. Logs can be + handled by any number of handlers, e.g., FileHandler, StreamHandler, etc., + provided in the `handlers` list. + + Parameters + ---------- + log_level + The level of logging to be recorded. Can be defined either as the + integer level or the logging. values in line with the definitions + of the logging module. + (see - https://docs.python.org/3/library/logging.html#levels) + handlers + List of handler instances to be added to the logger. Each handler + instance must be a subclass of `logging.Handler`. Default is an + empty list, and in this case, basicConfig with `log_level`, + `log_format`, and `date_format` is used. + log_format + The format of the log message. If not provided, a default format + `'%(asctime)s %(levelname)s %(name)s: %(message)s'` is used. + date_format + The format of the date in the log message. If not provided, a default + format `'%Y-%m-%d %H:%M:%S'` is used. + + Returns + ------- + None + The logger created by this function is available in any other modules + by using `logging.getLogger(__name__)` at the global scope level in a + module (i.e., below imports, not in a function). + + Raises + ------ + ValueError + If any item in the `handlers` list is not an instance of + `logging.Handler`. + + Examples + -------- + >>> file_handler = logging.FileHandler('logfile.log') + >>> rich_handler = RichHandler() + >>> init_logger_advanced( + ... logging.DEBUG, + ... [file_handler, rich_handler], + ... "%(levelname)s: %(message)s", + ... "%H:%M:%S" + ... ) + """ + # Set default log format and date format if not provided + if log_format is None: + log_format = '%(asctime)s %(levelname)s %(name)s: %(message)s' + if date_format is None: + date_format = '%Y-%m-%d %H:%M:%S' + + # Prepare a formatter + formatter = logging.Formatter(log_format, date_format) + + # Create a logger + logger = logging.getLogger(__name__) + logger.setLevel(log_level) + + # Check if handlers is None, if so assign an empty list to it + if handlers is None: + handlers = [] + + # Validate each handler + for handler in handlers: + if not isinstance(handler, logging.Handler): + msg = ( + f'Handler {handler} is not an instance of ' + f'logging.Handler or its subclasses' + ) + raise ValueError( + msg, + ) + + handler.setFormatter(formatter) + logger.addHandler(handler) + + # If no handlers provided, use basicConfig + if not handlers: + logging.basicConfig( + level=log_level, + format=log_format, + datefmt=date_format, + ) + + logger.debug('Initialised logger for pipeline.') + + def timer_args( name: str, logger: Optional[Callable[[str], None]] = logger.info, From 51f252f749aec48e0768df6e6a7e566b10a738d9 Mon Sep 17 00:00:00 2001 From: dombean <46692370+dombean@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:10:08 +0100 Subject: [PATCH 2/4] Modified CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1b0a3f3..23cb2c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ and this project adheres to [semantic versioning](https://semver.org/spec/v2.0.0 - Add the helpers_spark.py and test_helpers_spark.py modules from cprices-utils. - Add logging.py and test_logging.py module from cprices-utils. - Add the helpers_python.py and test_helpers_python.py modules from cprices-utils. +- Add `init_logger_advanced` in `helpers/logging.py` module. ### Changed From b3b41da1e1ca58de6aa9d6050b6257527621b90e Mon Sep 17 00:00:00 2001 From: dombean <46692370+dombean@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:26:22 +0100 Subject: [PATCH 3/4] Added unit tests for init_logger_advanced --- tests/helpers/test_logging.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/helpers/test_logging.py b/tests/helpers/test_logging.py index 5450ee9..fa10d87 100644 --- a/tests/helpers/test_logging.py +++ b/tests/helpers/test_logging.py @@ -10,6 +10,7 @@ ) from rdsa_utils.helpers.logging import ( + init_logger_advanced, timer_args, print_full_table_and_raise_error, ) @@ -152,3 +153,37 @@ class TestAddWarningMessageToFunction: def test_expected(self): """Test expected behaviour.""" pass + + +class TestInitLoggerAdvanced: + """Tests for init_logger_advanced function.""" + + def test_logger_with_no_handler(self, caplog): + """Test whether a logger is properly initialized with no handlers.""" + caplog.set_level(logging.DEBUG) + init_logger_advanced(logging.DEBUG) + assert caplog.records[0].levelname == "DEBUG" + + def test_logger_with_handlers(self, caplog): + """Test whether a logger is properly initialized with a valid handler.""" + caplog.set_level(logging.DEBUG) + handler = logging.FileHandler("logfile.log") + handlers = [handler] + init_logger_advanced(logging.DEBUG, handlers) + + logger = logging.getLogger("rdsa_utils.helpers.logging") + + assert caplog.records[0].levelname == "DEBUG" + assert any(isinstance(h, type(handler)) for h in logger.handlers) + + def test_logger_with_invalid_handler(self): + """Test whether a ValueError is raised when an invalid handler is provided.""" + log_level = logging.DEBUG + invalid_handler = "I am not a handler" + handlers = [invalid_handler] + with pytest.raises(ValueError) as exc_info: + init_logger_advanced(log_level, handlers) + assert ( + str(exc_info.value) + == f"Handler {invalid_handler} is not an instance of logging.Handler or its subclasses" + ) From 420efba59e2eece5da3854b2a9bfd473f658cda6 Mon Sep 17 00:00:00 2001 From: dombean <46692370+dombean@users.noreply.github.com> Date: Mon, 24 Jul 2023 16:26:37 +0100 Subject: [PATCH 4/4] Added unit tests for init_logger_advanced --- tests/helpers/test_logging.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/helpers/test_logging.py b/tests/helpers/test_logging.py index fa10d87..a92c810 100644 --- a/tests/helpers/test_logging.py +++ b/tests/helpers/test_logging.py @@ -162,28 +162,28 @@ def test_logger_with_no_handler(self, caplog): """Test whether a logger is properly initialized with no handlers.""" caplog.set_level(logging.DEBUG) init_logger_advanced(logging.DEBUG) - assert caplog.records[0].levelname == "DEBUG" + assert caplog.records[0].levelname == 'DEBUG' def test_logger_with_handlers(self, caplog): """Test whether a logger is properly initialized with a valid handler.""" caplog.set_level(logging.DEBUG) - handler = logging.FileHandler("logfile.log") + handler = logging.FileHandler('logfile.log') handlers = [handler] init_logger_advanced(logging.DEBUG, handlers) - logger = logging.getLogger("rdsa_utils.helpers.logging") + logger = logging.getLogger('rdsa_utils.helpers.logging') - assert caplog.records[0].levelname == "DEBUG" + assert caplog.records[0].levelname == 'DEBUG' assert any(isinstance(h, type(handler)) for h in logger.handlers) def test_logger_with_invalid_handler(self): """Test whether a ValueError is raised when an invalid handler is provided.""" log_level = logging.DEBUG - invalid_handler = "I am not a handler" + invalid_handler = 'I am not a handler' handlers = [invalid_handler] with pytest.raises(ValueError) as exc_info: init_logger_advanced(log_level, handlers) assert ( str(exc_info.value) - == f"Handler {invalid_handler} is not an instance of logging.Handler or its subclasses" + == f'Handler {invalid_handler} is not an instance of logging.Handler or its subclasses' )